From 3f9cb3c5a66aec74d6429050339d1af90c0179aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adam=20Zieli=C5=84ski?=
Date: Fri, 7 Aug 2026 12:05:20 +0200
Subject: [PATCH] Remove the Native APIs extension and WASM publisher
---
.github/workflows/docs.yml | 14 -
.../native-apis-playground-extension.yml | 761 --
.github/workflows/native-apis.yml | 52 -
.github/workflows/publish.yml | 1 +
.gitignore | 1 -
CHANGELOG.md | 187 -
README.md | 10 -
bin/benchmark-native-apis.php | 4473 -------
bin/build-reference.php | 3 +-
bin/summarize-native-api-benchmark.php | 185 -
.../Tests/DataLiberationHTMLProcessorTest.php | 27 +-
.../Tests/URLInTextProcessorTest.php | 22 -
.../class-nativeurlintextprocessorwrapper.php | 241 -
.../URL/class-urlintextprocessor.php | 28 +-
.../class-dataliberationhtmlprocessor.php | 215 +-
.../HTML/PHP/class-wp-html-php-processor.php | 2 +-
components/HTML/README.md | 13 +-
.../HTML/Tests/NativeHTMLConformanceTest.php | 4007 ------
...class-wp-html-native-processor-wrapper.php | 16 -
...s-wp-html-native-tag-processor-wrapper.php | 16 -
components/HTML/class-wp-html-processor.php | 20 +-
.../HTML/class-wp-html-tag-processor.php | 18 +-
components/XML/README.md | 11 +-
.../XML/Tests/NativeXMLConformanceTest.php | 3223 -----
.../XML/class-xmlnativecursorprocessor.php | 12 -
components/XML/class-xmlprocessor.php | 16 +-
docs/assets/style.css | 115 +-
docs/index.html | 13 +-
docs/learn/01-rewriting-html.html | 3 +-
docs/learn/02-streaming-archives.html | 3 +-
docs/learn/03-importing-content.html | 3 +-
docs/learn/04-talking-to-the-network.html | 3 +-
docs/learn/index.html | 3 +-
docs/learn/quickstart.html | 3 +-
docs/learn/recap.html | 3 +-
docs/native-apis.html | 106 -
docs/native-php-extension.html | 139 -
docs/native-rust-apis.md | 394 -
docs/reference/index.html | 9 +-
extensions/native-apis/.gitignore | 1 -
extensions/native-apis/Cargo.lock | 1683 ---
extensions/native-apis/Cargo.toml | 20 -
extensions/native-apis/README.md | 455 -
extensions/native-apis/build-extension.sh | 38 -
.../native-apis/build-playground-extension.sh | 27 -
extensions/native-apis/config.m4 | 9 -
extensions/native-apis/native_apis_shim.c | 668 -
.../native-apis/playground/blueprint.json | 20 -
extensions/native-apis/src/html.rs | 8569 ------------
extensions/native-apis/src/lib.rs | 43 -
extensions/native-apis/src/url_text.rs | 654 -
extensions/native-apis/src/xml.rs | 10846 ----------------
.../native-apis/tests/verify-native-apis.php | 1949 ---
53 files changed, 42 insertions(+), 39311 deletions(-)
delete mode 100644 .github/workflows/native-apis-playground-extension.yml
delete mode 100644 .github/workflows/native-apis.yml
delete mode 100644 bin/benchmark-native-apis.php
delete mode 100644 bin/summarize-native-api-benchmark.php
delete mode 100644 components/DataLiberation/URL/class-nativeurlintextprocessorwrapper.php
delete mode 100644 components/HTML/Tests/NativeHTMLConformanceTest.php
delete mode 100644 components/HTML/class-wp-html-native-processor-wrapper.php
delete mode 100644 components/HTML/class-wp-html-native-tag-processor-wrapper.php
delete mode 100644 components/XML/Tests/NativeXMLConformanceTest.php
delete mode 100644 components/XML/class-xmlnativecursorprocessor.php
delete mode 100644 docs/native-apis.html
delete mode 100644 docs/native-php-extension.html
delete mode 100644 docs/native-rust-apis.md
delete mode 100644 extensions/native-apis/.gitignore
delete mode 100644 extensions/native-apis/Cargo.lock
delete mode 100644 extensions/native-apis/Cargo.toml
delete mode 100644 extensions/native-apis/README.md
delete mode 100755 extensions/native-apis/build-extension.sh
delete mode 100755 extensions/native-apis/build-playground-extension.sh
delete mode 100644 extensions/native-apis/config.m4
delete mode 100644 extensions/native-apis/native_apis_shim.c
delete mode 100644 extensions/native-apis/playground/blueprint.json
delete mode 100644 extensions/native-apis/src/html.rs
delete mode 100644 extensions/native-apis/src/lib.rs
delete mode 100644 extensions/native-apis/src/url_text.rs
delete mode 100644 extensions/native-apis/src/xml.rs
delete mode 100644 extensions/native-apis/tests/verify-native-apis.php
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index fb0fd4e36..3a0b2211e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -46,20 +46,6 @@ jobs:
-x "*/Tests/*" "*/tests/*" "*/.git/*" "*/.github/*" "*/node_modules/*"
php bin/build-reference.php
- - name: Preserve Native APIs Playground release history
- env:
- EXTENSION_PATH: wp_native_apis-wasm-extension
- run: |
- set -euo pipefail
- pages_dir="$(mktemp -d)"
- if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
- git clone --depth=1 --branch gh-pages "https://github.com/${GITHUB_REPOSITORY}.git" "$pages_dir"
- if [ -d "$pages_dir/$EXTENSION_PATH" ]; then
- rm -rf "docs/$EXTENSION_PATH"
- cp -R "$pages_dir/$EXTENSION_PATH" "docs/$EXTENSION_PATH"
- fi
- fi
-
- uses: actions/upload-pages-artifact@v3
with:
path: ./docs
diff --git a/.github/workflows/native-apis-playground-extension.yml b/.github/workflows/native-apis-playground-extension.yml
deleted file mode 100644
index d1df45a4e..000000000
--- a/.github/workflows/native-apis-playground-extension.yml
+++ /dev/null
@@ -1,761 +0,0 @@
-name: Native APIs Playground Extension
-
-on:
- pull_request:
- branches:
- - trunk
- paths:
- - 'extensions/native-apis/**'
- - 'bin/benchmark-native-apis.php'
- - 'bin/summarize-native-api-benchmark.php'
- - '.github/workflows/native-apis-playground-extension.yml'
- push:
- branches:
- - trunk
- paths:
- - 'extensions/native-apis/**'
- - 'bin/benchmark-native-apis.php'
- - 'bin/summarize-native-api-benchmark.php'
- - '.github/workflows/native-apis-playground-extension.yml'
- workflow_dispatch:
- inputs:
- retention-days:
- description: 'Actions artifact retention in days'
- required: false
- default: '30'
-
-# Disable permissions for all available scopes by default.
-# Any needed permissions should be configured at the job level.
-permissions: {}
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- build:
- name: Build PHP.wasm extension
- runs-on: ubuntu-latest
- timeout-minutes: 90
- permissions:
- contents: read
- env:
- PHP_WASM_VERSIONS: '8.0,8.1,8.2,8.3,8.4,8.5'
-
- steps:
- - uses: actions/checkout@v4
-
- - uses: actions/setup-node@v4
- with:
- node-version: '24'
-
- - name: Build wp_native_apis PHP.wasm extension
- run: |
- extensions/native-apis/build-playground-extension.sh build/wp_native_apis-wasm-extension
-
- - name: Normalize Playground extension manifest
- env:
- EXTENSION_VERSION: ${{ github.sha }}
- run: |
- node <<'NODE'
- const fs = require('node:fs');
- const path = require('node:path');
-
- const manifestPath = path.join('build', 'wp_native_apis-wasm-extension', 'manifest.json');
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
- manifest.version = process.env.EXTENSION_VERSION;
- manifest.mode = 'php-extension';
- fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
- NODE
-
- - name: Verify published manifest shape
- run: |
- node <<'NODE'
- const fs = require('node:fs');
- const path = require('node:path');
-
- const root = path.join('build', 'wp_native_apis-wasm-extension');
- const manifest = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8'));
- if (manifest.name !== 'wp_native_apis') {
- throw new Error(`Unexpected extension name: ${manifest.name}`);
- }
- if (manifest.mode !== 'php-extension') {
- throw new Error(`Unexpected manifest mode: ${manifest.mode}`);
- }
- const expectedPhpVersions = process.env.PHP_WASM_VERSIONS.split(',').map((version) => version.trim()).filter(Boolean);
- if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length !== expectedPhpVersions.length) {
- throw new Error(`Expected PHP.wasm artifacts for ${expectedPhpVersions.join(', ')}.`);
- }
- const seenPhpVersions = new Set();
- for (const artifact of manifest.artifacts) {
- if (!artifact || typeof artifact.phpVersion !== 'string' || !expectedPhpVersions.includes(artifact.phpVersion) || !artifact.sourcePath) {
- throw new Error(`Invalid artifact entry: ${JSON.stringify(artifact)}`);
- }
- if (seenPhpVersions.has(artifact.phpVersion)) {
- throw new Error(`Duplicate artifact for PHP ${artifact.phpVersion}.`);
- }
- seenPhpVersions.add(artifact.phpVersion);
- if ('file' in artifact || 'sha256' in artifact) {
- throw new Error(`Manifest uses retired pre-PR-3580 fields: ${JSON.stringify(artifact)}`);
- }
- const artifactPath = path.join(root, artifact.sourcePath);
- if (!fs.existsSync(artifactPath)) {
- throw new Error(`Manifest references missing artifact: ${artifactPath}`);
- }
- }
- for (const phpVersion of expectedPhpVersions) {
- if (!seenPhpVersions.has(phpVersion)) {
- throw new Error(`Missing artifact for PHP ${phpVersion}.`);
- }
- }
- NODE
-
- - name: Write checksums
- working-directory: build/wp_native_apis-wasm-extension
- run: sha256sum *.so > SHA256SUMS
-
- - name: Upload Playground extension
- uses: actions/upload-artifact@v4
- with:
- name: wp-native-apis-playground-extension
- path: build/wp_native_apis-wasm-extension/
- if-no-files-found: error
- retention-days: ${{ github.event.inputs.retention-days || '30' }}
-
- benchmark-host-extension:
- name: Benchmark host PHP extension
- runs-on: ubuntu-latest
- needs: build
- timeout-minutes: 90
- permissions:
- contents: read
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.3'
- extensions: mbstring, json
- coverage: none
- tools: composer:v2
-
- - name: Setup Rust
- uses: dtolnay/rust-toolchain@stable
-
- - name: Install native build dependencies
- run: |
- sudo apt-get update
- sudo apt-get install -y clang libclang-dev
- php-config --version
-
- - name: Install Composer dependencies
- run: composer install --prefer-dist --no-progress --no-suggest
-
- - name: Build native extension
- run: extensions/native-apis/build-extension.sh
-
- - name: Run native API benchmarks
- run: |
- mkdir -p build/native-api-benchmark
- php -d extension=extensions/native-apis/target/release/libwp_native_apis.so \
- bin/benchmark-native-apis.php \
- --iterations=50 \
- --mode=both \
- --disable-native-defaults \
- --require-native > build/native-api-benchmark/benchmark.json
- php bin/summarize-native-api-benchmark.php \
- build/native-api-benchmark/benchmark.json \
- build/native-api-benchmark/benchmark-summary.json
-
- - name: Upload benchmark results
- uses: actions/upload-artifact@v4
- with:
- name: wp-native-apis-host-benchmark
- path: build/native-api-benchmark/
- if-no-files-found: error
- retention-days: ${{ github.event.inputs.retention-days || '30' }}
-
- publish:
- name: Publish static extension bundle to GitHub Pages
- if: github.ref == 'refs/heads/trunk' && (github.event_name == 'workflow_dispatch' || github.event_name == 'push')
- runs-on: ubuntu-latest
- needs:
- - build
- - benchmark-host-extension
- timeout-minutes: 30
- permissions:
- contents: write
- pages: write
- id-token: write
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- concurrency:
- group: pages
- cancel-in-progress: true
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.1'
- tools: composer
- coverage: none
-
- - name: Install docs dependencies
- run: composer install --no-dev --optimize-autoloader --no-progress
-
- - name: Build docs site
- run: |
- mkdir -p docs/assets
- rm -f docs/assets/php-toolkit.zip
- zip -qr docs/assets/php-toolkit.zip components vendor bootstrap.php composer.json \
- -x "*/Tests/*" "*/tests/*" "*/.git/*" "*/.github/*" "*/node_modules/*"
- php bin/build-reference.php
-
- - name: Download packaged extension
- uses: actions/download-artifact@v4
- with:
- name: wp-native-apis-playground-extension
- path: build/wp_native_apis-wasm-extension
-
- - name: Download host benchmark results
- uses: actions/download-artifact@v4
- with:
- name: wp-native-apis-host-benchmark
- path: build/native-api-benchmark
-
- - name: Publish static extension bundle to gh-pages
- env:
- BUNDLE_DIR: build/wp_native_apis-wasm-extension
- BENCHMARK_DIR: build/native-api-benchmark
- DOCS_DIR: docs
- EXTENSION_PATH: wp_native_apis-wasm-extension
- GITHUB_TOKEN: ${{ github.token }}
- SITE_BRANCH: gh-pages
- VERSION_PATH: ${{ github.sha }}
- run: |
- set -euo pipefail
-
- pages_dir="$(mktemp -d)"
- git init "$pages_dir"
- git -C "$pages_dir" remote add origin "https://github.com/${GITHUB_REPOSITORY}.git"
- auth_header="$(printf 'x-access-token:%s' "${GITHUB_TOKEN}" | base64 -w 0)"
- git -C "$pages_dir" config http.https://github.com/.extraheader "AUTHORIZATION: basic ${auth_header}"
- git -C "$pages_dir" config user.name "github-actions[bot]"
- git -C "$pages_dir" config user.email "41898282+github-actions[bot]@users.noreply.github.com"
-
- if git -C "$pages_dir" ls-remote --exit-code --heads origin "$SITE_BRANCH" >/dev/null 2>&1; then
- git -C "$pages_dir" fetch origin "$SITE_BRANCH"
- git -C "$pages_dir" checkout -B "$SITE_BRANCH" FETCH_HEAD
- else
- git -C "$pages_dir" checkout --orphan "$SITE_BRANCH"
- fi
-
- mkdir -p "$pages_dir/$EXTENSION_PATH"
- rm -rf "$pages_dir/$EXTENSION_PATH/$VERSION_PATH" "$pages_dir/$EXTENSION_PATH/latest"
- mkdir -p "$pages_dir/$EXTENSION_PATH/$VERSION_PATH" "$pages_dir/$EXTENSION_PATH/latest"
- cp -R "$BUNDLE_DIR"/. "$pages_dir/$EXTENSION_PATH/$VERSION_PATH/"
- cp -R "$BUNDLE_DIR"/. "$pages_dir/$EXTENSION_PATH/latest/"
- if [ -d "$BENCHMARK_DIR" ]; then
- mkdir -p "$pages_dir/$EXTENSION_PATH/$VERSION_PATH/benchmarks" "$pages_dir/$EXTENSION_PATH/latest/benchmarks"
- cp "$BENCHMARK_DIR"/benchmark.json "$pages_dir/$EXTENSION_PATH/$VERSION_PATH/benchmarks/benchmark.json"
- cp "$BENCHMARK_DIR"/benchmark-summary.json "$pages_dir/$EXTENSION_PATH/$VERSION_PATH/benchmarks/benchmark-summary.json"
- cp "$BENCHMARK_DIR"/benchmark.json "$pages_dir/$EXTENSION_PATH/latest/benchmarks/benchmark.json"
- cp "$BENCHMARK_DIR"/benchmark-summary.json "$pages_dir/$EXTENSION_PATH/latest/benchmarks/benchmark-summary.json"
- fi
- touch "$pages_dir/.nojekyll"
-
- PAGES_DIR="$pages_dir" PUBLISHED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" node <<'NODE'
- const childProcess = require('node:child_process');
- const fs = require('node:fs');
- const os = require('node:os');
- const path = require('node:path');
-
- const pagesDir = process.env.PAGES_DIR;
- const extensionPath = process.env.EXTENSION_PATH;
- const versionPath = process.env.VERSION_PATH;
- const repository = process.env.GITHUB_REPOSITORY;
- const [owner, repo] = repository.split('/');
- const root = path.join(pagesDir, extensionPath);
- const siteRootUrl = `https://${owner.toLowerCase()}.github.io/${repo}`;
- const rootUrl = `https://${owner.toLowerCase()}.github.io/${repo}/${extensionPath}`;
- const releasesPath = path.join(root, 'releases.json');
- const indexPath = path.join(root, 'index.html');
-
- function readJson(filePath, fallback) {
- try {
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
- } catch (error) {
- return fallback;
- }
- }
-
- function escapeHtml(value) {
- return String(value)
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"');
- }
-
- function comparePhpVersions(left, right) {
- const leftParts = String(left).split('.').map((part) => Number.parseInt(part, 10) || 0);
- const rightParts = String(right).split('.').map((part) => Number.parseInt(part, 10) || 0);
- return (leftParts[0] - rightParts[0]) || (leftParts[1] - rightParts[1]);
- }
-
- function lastPublishedAt(relativeManifestPath) {
- try {
- return childProcess.execFileSync(
- 'git',
- ['-C', pagesDir, 'log', '-1', '--format=%cI', '--', relativeManifestPath],
- { encoding: 'utf8' }
- ).trim();
- } catch (error) {
- return '';
- }
- }
-
- const previousReleases = readJson(releasesPath, []);
- const sanitizedRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'wp-native-apis-pages-'));
-
- function isRegularFile(filePath) {
- try {
- return fs.lstatSync(filePath).isFile();
- } catch (error) {
- return false;
- }
- }
-
- function isReleaseDirectoryName(name) {
- return name === 'latest' || /^[0-9a-f]{40}$/.test(name);
- }
-
- function isArtifactSourcePath(sourcePath) {
- return typeof sourcePath === 'string' && /^wp_native_apis-php[0-9.]+-jspi\.so$/.test(sourcePath);
- }
-
- /*
- * Treat the gh-pages branch as an append-only release history, not
- * as a trusted source tree. Only expected release directories and
- * files are copied into the branch update and Pages artifact.
- */
- for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
- if (!entry.isDirectory() || !isReleaseDirectoryName(entry.name)) {
- continue;
- }
-
- const releaseDirectory = path.join(root, entry.name);
- const manifestPath = path.join(releaseDirectory, 'manifest.json');
- const checksumPath = path.join(releaseDirectory, 'SHA256SUMS');
- if (!isRegularFile(manifestPath) || !isRegularFile(checksumPath)) {
- continue;
- }
-
- const manifest = readJson(manifestPath, null);
- if (!manifest || manifest.name !== 'wp_native_apis' || manifest.mode !== 'php-extension') {
- continue;
- }
-
- const artifacts = Array.isArray(manifest.artifacts)
- ? manifest.artifacts.filter((artifact) => {
- if (
- !artifact ||
- typeof artifact.phpVersion !== 'string' ||
- !/^[0-9]+\.[0-9]+$/.test(artifact.phpVersion) ||
- !isArtifactSourcePath(artifact.sourcePath)
- ) {
- return false;
- }
-
- return isRegularFile(path.join(releaseDirectory, artifact.sourcePath));
- })
- : [];
-
- if (artifacts.length === 0) {
- continue;
- }
-
- const sanitizedDirectory = path.join(sanitizedRoot, entry.name);
- fs.mkdirSync(sanitizedDirectory, { recursive: true });
- fs.writeFileSync(
- path.join(sanitizedDirectory, 'manifest.json'),
- `${JSON.stringify({ ...manifest, artifacts }, null, 2)}\n`
- );
- fs.copyFileSync(checksumPath, path.join(sanitizedDirectory, 'SHA256SUMS'));
-
- for (const artifact of artifacts) {
- fs.copyFileSync(
- path.join(releaseDirectory, artifact.sourcePath),
- path.join(sanitizedDirectory, artifact.sourcePath)
- );
- }
-
- const benchmarkDirectory = path.join(releaseDirectory, 'benchmarks');
- const benchmarkJson = path.join(benchmarkDirectory, 'benchmark.json');
- const benchmarkSummaryJson = path.join(benchmarkDirectory, 'benchmark-summary.json');
- if (isRegularFile(benchmarkJson) && isRegularFile(benchmarkSummaryJson)) {
- const sanitizedBenchmarkDirectory = path.join(sanitizedDirectory, 'benchmarks');
- fs.mkdirSync(sanitizedBenchmarkDirectory, { recursive: true });
- fs.copyFileSync(benchmarkJson, path.join(sanitizedBenchmarkDirectory, 'benchmark.json'));
- fs.copyFileSync(benchmarkSummaryJson, path.join(sanitizedBenchmarkDirectory, 'benchmark-summary.json'));
- }
- }
-
- fs.rmSync(root, { recursive: true, force: true });
- fs.mkdirSync(root, { recursive: true });
- for (const entry of fs.readdirSync(sanitizedRoot, { withFileTypes: true })) {
- fs.cpSync(path.join(sanitizedRoot, entry.name), path.join(root, entry.name), { recursive: true });
- }
-
- const previousByVersion = new Map(
- Array.isArray(previousReleases)
- ? previousReleases.map((release) => [release.version, release])
- : []
- );
-
- const releases = fs.readdirSync(root, { withFileTypes: true })
- .filter((entry) => entry.isDirectory() && /^[0-9a-f]{40}$/.test(entry.name))
- .map((entry) => {
- const version = entry.name;
- const existing = previousByVersion.get(version) || {};
- const relativeManifestPath = path.join(extensionPath, version, 'manifest.json');
- const manifestPath = path.join(root, version, 'manifest.json');
- const manifest = readJson(manifestPath, {});
- const manifestUrl = `${rootUrl}/${version}/manifest.json`;
- const benchmarkPath = path.join(root, version, 'benchmarks', 'benchmark.json');
- const benchmarkSummaryPath = path.join(root, version, 'benchmarks', 'benchmark-summary.json');
- const benchmarkSummary = readJson(benchmarkSummaryPath, null);
- const blueprintUrl = `https://raw.githubusercontent.com/${repository}/${version}/extensions/native-apis/playground/blueprint.json`;
- const playgroundUrl = (phpVersion) => `https://playground.wordpress.net/?php=${encodeURIComponent(phpVersion)}&php-extension=${encodeURIComponent(manifestUrl)}&blueprint-url=${encodeURIComponent(blueprintUrl)}`;
- const artifacts = Array.isArray(manifest.artifacts)
- ? manifest.artifacts.map((artifact) => ({
- phpVersion: artifact.phpVersion,
- sourcePath: artifact.sourcePath,
- url: `${rootUrl}/${version}/${artifact.sourcePath}`,
- testInPlayground: playgroundUrl(artifact.phpVersion),
- })).sort((left, right) => comparePhpVersions(left.phpVersion, right.phpVersion))
- : [];
- const preferredArtifact = artifacts[artifacts.length - 1] || null;
-
- return {
- version,
- name: manifest.name || 'wp_native_apis',
- phpVersions: artifacts.map((artifact) => artifact.phpVersion).filter(Boolean),
- artifacts,
- publishedAt: version === versionPath
- ? process.env.PUBLISHED_AT
- : existing.publishedAt || lastPublishedAt(relativeManifestPath),
- manifest: manifestUrl,
- checksums: `${rootUrl}/${version}/SHA256SUMS`,
- commit: `https://github.com/${repository}/commit/${version}`,
- workflowRun: version === versionPath
- ? `https://github.com/${repository}/actions/runs/${process.env.GITHUB_RUN_ID}`
- : existing.workflowRun || '',
- benchmark: isRegularFile(benchmarkPath)
- ? `${rootUrl}/${version}/benchmarks/benchmark.json`
- : existing.benchmark || '',
- benchmarkSummaryUrl: isRegularFile(benchmarkSummaryPath)
- ? `${rootUrl}/${version}/benchmarks/benchmark-summary.json`
- : existing.benchmarkSummaryUrl || '',
- benchmarkSummary: benchmarkSummary && Array.isArray(benchmarkSummary.top)
- ? benchmarkSummary.top
- : existing.benchmarkSummary || [],
- testInPlayground: preferredArtifact ? preferredArtifact.testInPlayground : '',
- };
- })
- .sort((a, b) => {
- if (a.publishedAt && b.publishedAt) {
- return b.publishedAt.localeCompare(a.publishedAt);
- }
- return b.version.localeCompare(a.version);
- });
-
- fs.writeFileSync(releasesPath, `${JSON.stringify(releases, null, 2)}\n`);
-
- function formatSeconds(value) {
- if (typeof value !== 'number' || !Number.isFinite(value)) {
- return 'n/a';
- }
- if (value < 0.001) {
- return `${(value * 1000000).toFixed(0)} µs`;
- }
- if (value < 1) {
- return `${(value * 1000).toFixed(1)} ms`;
- }
- return `${value.toFixed(2)} s`;
- }
-
- function formatDate(value) {
- if (!value) {
- return 'unknown';
- }
- const parsed = new Date(value);
- if (Number.isNaN(parsed.getTime())) {
- return String(value).slice(0, 10);
- }
- return parsed.toISOString().slice(0, 10);
- }
-
- function renderBenchmarkSummary(release) {
- const items = Array.isArray(release.benchmarkSummary)
- ? release.benchmarkSummary.slice(0, 3)
- : [];
- if (!items.length) {
- return 'No benchmark summary ';
- }
-
- return `${items.map((item) => `
-
- ${escapeHtml(item.speedup || 'n/a')}×
- ${escapeHtml(item.label || item.name)}
- PHP ${escapeHtml(formatSeconds(item.phpWallSeconds))} · native ${escapeHtml(formatSeconds(item.nativeWallSeconds))}
-
- `).join('')}
`;
- }
-
- function renderArtifactLinks(release) {
- const artifacts = Array.isArray(release.artifacts) ? release.artifacts : [];
- const testLinks = artifacts.map((artifact) =>
- `PHP ${escapeHtml(artifact.phpVersion)} `
- ).join(', ');
- const downloadLinks = artifacts.map((artifact) =>
- `PHP ${escapeHtml(artifact.phpVersion)} `
- ).join(', ');
-
- return [
- `Manifest `,
- testLinks ? `Test: ${testLinks}` : '',
- downloadLinks ? `Download: ${downloadLinks}` : '',
- `SHA256 `,
- ].filter(Boolean).join(' · ');
- }
-
- const latestRelease = releases[0] || null;
- const rows = releases.map((release, index) => `
-
- ${escapeHtml(formatDate(release.publishedAt))} ${index === 0 ? 'Latest ' : ''}
- ${escapeHtml(release.phpVersions.join(', ') || 'unknown')}
- ${renderBenchmarkSummary(release)}
- ${release.testInPlayground ? `Test latest PHP · ` : ''}${renderArtifactLinks(release)}
-
- `).join('');
-
- fs.writeFileSync(indexPath, `
-
-
-
-
- wp_native_apis PHP.wasm Extension Releases
-
-
-
-
-
-
- Native APIs WASM releases for Playground
-
- Published PHP.wasm builds of the experimental wp_native_apis extension for WordPress Playground.
- Use this page to test the latest build, pin an immutable manifest, or inspect release artifacts.
-
-
-
-
-
- PHP.wasm builds for Playground Web and CLI
-
-
-
-
-
-
- Released on
- Playground PHP versions
- CI speed snapshot
- Links
-
-
-
- ${rows || 'No releases published yet. '}
-
-
-
-
-
-
-
-
-
- `);
- NODE
-
- git -C "$pages_dir" add .nojekyll "$EXTENSION_PATH"
- if git -C "$pages_dir" diff --cached --quiet; then
- echo "No gh-pages changes to publish."
- else
- git -C "$pages_dir" commit -m "Publish wp_native_apis WASM extension ${VERSION_PATH}"
- git -C "$pages_dir" push origin "HEAD:$SITE_BRANCH"
- fi
-
- owner="${GITHUB_REPOSITORY_OWNER,,}"
- repo="${GITHUB_REPOSITORY#*/}"
- echo "Published release index: https://${owner}.github.io/${repo}/${EXTENSION_PATH}/"
- echo "Published latest manifest: https://${owner}.github.io/${repo}/${EXTENSION_PATH}/latest/manifest.json"
- echo "Published immutable manifest: https://${owner}.github.io/${repo}/${EXTENSION_PATH}/${VERSION_PATH}/manifest.json"
-
- rm -rf "$DOCS_DIR/$EXTENSION_PATH"
- mkdir -p "$DOCS_DIR"
- cp -R "$pages_dir/$EXTENSION_PATH" "$DOCS_DIR/$EXTENSION_PATH"
- touch "$DOCS_DIR/.nojekyll"
-
- - uses: actions/upload-pages-artifact@v3
- with:
- path: ./docs
-
- - id: deployment
- uses: actions/deploy-pages@v4
diff --git a/.github/workflows/native-apis.yml b/.github/workflows/native-apis.yml
deleted file mode 100644
index c49c141f5..000000000
--- a/.github/workflows/native-apis.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-name: Native APIs
-
-on:
- push:
- branches:
- - trunk
- pull_request:
- paths:
- - '.github/workflows/native-apis.yml'
- - 'components/DataLiberation/URL/**'
- - 'components/HTML/**'
- - 'components/XML/**'
- - 'extensions/native-apis/**'
-
-jobs:
- build-and-verify:
- name: Build and verify PHP extension
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.3'
- extensions: mbstring, json
- coverage: none
- tools: composer:v2
-
- - name: Setup Rust
- uses: dtolnay/rust-toolchain@stable
-
- - name: Install native build dependencies
- run: |
- sudo apt-get update
- sudo apt-get install -y clang libclang-dev
- php-config --version
-
- - name: Install Composer dependencies
- run: composer install --prefer-dist --no-progress --no-suggest
-
- - name: Run Rust tests
- working-directory: extensions/native-apis
- run: cargo test
-
- - name: Build native extension
- run: extensions/native-apis/build-extension.sh
-
- - name: Verify native extension
- run: php -d extension=extensions/native-apis/target/release/libwp_native_apis.so extensions/native-apis/tests/verify-native-apis.php
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 4e5d073e0..f3e9c5081 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -70,6 +70,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
+ # Ignore historical WASM extension tags when calculating package releases.
git tag --list 'wp-native-apis-wasm-*' | xargs -r git tag -d
$(composer global config bin-dir --absolute)/monorepo-builder release v${{ steps.semver.outputs.new_version }}
diff --git a/.gitignore b/.gitignore
index c8f74e912..3c130c40e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,6 @@ node_modules
.cursor
rest/
dist/
-build/
outdir/
.idea/
.my-notes-git/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdd5ba3b9..6652948e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,190 +5,3 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
- Placeholder section for upcoming changes. Entries are appended automatically by the release workflow.
-- Added a native `WordPress\DataLiberation\URL\NativeURLInTextProcessor` candidate scanner for URL-in-text detection.
-- Split public HTML, XML, and URL-in-text classes into small loaders plus PHP/native implementation sidecars so native defaults are isolated from pure PHP implementations.
-- Added public `URLInTextProcessor` support for native ASCII candidate scanning while preserving WHATWG validation.
-- Added a `--component=url` URL-in-text benchmark row for public and direct native scanner measurements.
-- Added native-default conformance coverage keeping `WP_HTML_Processor::create_full_parser()` on PHP fallback until native full-document semantics match public parser behavior.
-- Fixed native HTML pending attribute insertion serialization so multiple newly-added attributes, replaced pending attributes, and removed pending attributes match PHP fallback output.
-- Kept `DataLiberationHTMLProcessor` on PHP-backed HTML parsing because it depends on parser bookmarks and byte offsets that native delegates intentionally hide.
-- Fixed Markdown table production after cell extraction so row iteration does not skip remaining cells.
-- Documented native default controls for HTML and XML wrappers.
-- Added a `--disable-native-defaults` benchmark flag for forcing PHP fallback rows after the native extension is loaded.
-- Fixed native HTML class mutation composition when a fully removed class attribute is followed by adding a different class.
-- Fixed public native-backed XML bookmark seeks so later mutations replay PHP fallback state to the bookmarked token.
-- Fixed public native-backed XML bookmark spans so bookmarks beyond a mutation handoff remain seekable after native delegates fall back to PHP state.
-- Fixed public native-backed XML read-only scans so token iteration exposes the final `#complete` token like PHP fallback.
-- Increased public native-backed XML compact token batches to reduce native boundary crossings in large read-only token scans.
-- Optimized public native-backed XML compact token parsing by lazily decoding tag flags, depth, and cached attributes only when accessors need them.
-- Optimized public native-backed XML hot token rows by avoiding redundant lazy metadata resets during compact batch scans.
-- Optimized public native-backed XML hot token rows by avoiding unused flag and depth offset resets during compact batch scans.
-- Optimized native XML hot compact summary row construction for public native-backed token scans.
-- Optimized direct native XML token scans by lazily decoding text payloads only when text accessors need them.
-- Optimized direct native XML token scans by lazily materializing breadcrumbs only when breadcrumb accessors need them.
-- Optimized public native-backed XML token scans with a lightweight native cursor for safe complete-document reads.
-- Optimized public native-backed XML lightweight cursor row parsing by reusing queued compact batches locally.
-- Optimized public native-backed XML lightweight cursor accessors by using token metadata as the active-state sentinel.
-- Optimized public native-backed XML lightweight cursor batches by using cursor-specific native rows and inlined active-token checks.
-- Fixed direct native XML attribute reads so `get_attribute()` accepts the public namespace/local-name argument shape while preserving direct one-argument lookups.
-- Added direct native XML support for simple unprefixed current-tag attribute insertion, replacement, removal, and updated serialization.
-- Added direct native XML support for current-tag attribute insertion, replacement, and removal through in-scope namespace prefixes.
-- Added direct native XML support for current text, comment, and CDATA mutation with updated serialization.
-- Optimized public native-backed XML hot token row parsing by splitting the common tag-token path before non-tag token kind mapping.
-- Optimized public native-backed XML hot token row parsing by deriving non-tag token names from cached token types.
-- Optimized public native-backed XML hot token rows by lazily parsing non-tag compact-row fields and eagerly caching hot `id` attributes for tag rows.
-- Fixed public native-backed HTML class checks on closing tags so `has_class()` returns false like PHP fallback.
-- Enabled public native-backed HTML fragments for covered table tree-builder cases while keeping full-document parsing on PHP fallback.
-- Optimized native HTML tag-only scans by skipping discarded text and comment token construction in low-level tag processor paths.
-- Optimized low-level native HTML tag-only scans by skipping breadcrumb tracking when tree-builder state is not exposed.
-- Optimized public native-backed XML token batch parsing by avoiding per-token compact-row string copies.
-- Fixed native XML streaming reentrancy cursors so sliced resumes preserve child/sibling context while rejecting the pre-cursor parent closer like PHP fallback.
-- Added an HTML heading inventory summary API with a native Rust-backed fused path for heading-level outline audits.
-- Added an HTML ID inventory summary API with a native Rust-backed fused path for unique and duplicate ID audits.
-- Added an HTML attribute inventory summary API with a native Rust-backed fused path for attribute-name and decoded-value audits.
-- Added an HTML data-attribute inventory summary API with a native Rust-backed fused path for `data-*` usage audits.
-- Added an HTML ARIA attribute inventory summary API with a native Rust-backed fused path for `aria-*` accessibility audits.
-- Added an HTML class inventory summary API with a native Rust-backed fused path for class-attribute and unique-class audits.
-- Added an HTML resource inventory summary API with a native Rust-backed fused path for common link and media attribute audits.
-- Added an HTML image inventory summary API with a native Rust-backed fused path for image source, alt-text, and dimension audits.
-- Added an HTML script inventory summary API with a native Rust-backed fused path for script source, module, async/defer, and inline byte audits.
-- Added an XML ID inventory summary API with a native Rust-backed fused path for unique and duplicate ID audits.
-- Added an HTML form inventory summary API with a native Rust-backed fused path for form/control name audits.
-- Fixed public native-backed XML tag-token modifiable text after `next_tag()` so it matches PHP fallback semantics before mutating or serializing.
-- Fixed public native-backed HTML tag processor bookmarks to delegate bookmark lifecycle operations to the native processor.
-- Fixed public native-backed HTML processor bookmark seeking to delegate to the native processor.
-- Fixed public native-backed XML processor bookmarks to delegate bookmark lifecycle operations to the native processor.
-- Added an HTML tag inventory summary API with a native Rust-backed fused path for tag, closer, attribute, and unique-name audits.
-- Added an HTML prefix count batch API with a native Rust-backed incremental path for chunked tag and prefixed-attribute counting.
-- Optimized public HTML native-wrapper generic tag scans for unrestricted `next_tag()` queries.
-- Reduced public HTML native-wrapper dispatch overhead for unrestricted generic tag and prefixed-attribute cursor scans.
-- Added no-argument native HTML compact tag cursor entrypoints for common unrestricted scan modes.
-- Fixed public HTML native-default `next_tag()` query parsing for non-integer `match_offset` and invalid query types to match PHP fallback behavior.
-- Fixed public HTML native-default `next_tag()` query parsing so `tag_name` filters are honored when `tag_closers` is set to `visit`.
-- Fixed native HTML processing-instruction-looking comment parsing so valid targets expose `get_tag()` and invalid `...>` comments match PHP fallback metadata.
-- Fixed native HTML invalid closing tag and abruptly closed comment parsing to match PHP fallback token metadata.
-- Fixed native HTML CDATA-looking comments, whitespace-prefixed invalid closers, and dash-abrupt comments to match PHP fallback token metadata.
-- Fixed native HTML DOCTYPE token names so malformed and non-HTML doctypes expose the same `html` token name as PHP fallback.
-- Fixed native HTML invalid opening tags so non-alpha tag-name starts remain text tokens like PHP fallback.
-- Fixed native HTML invalid opening tag text boundaries, incomplete tag-like endings, and text-token character reference decoding to match PHP fallback.
-- Kept public HTML fragment processors on PHP fallback for tree-builder-sensitive list/table/omitted-tag fragments until native breadcrumbs model implied elements and closers.
-- Fixed native HTML processor breadcrumbs for table bodies/cells, table captions, colgroups, select option/optgroup groups, description lists, ruby text/parentheses, and repeated list item and paragraph starts by synthesizing selected implied tokens like PHP fallback.
-- Fixed native HTML processor EOF breadcrumbs so ordinary unclosed non-void elements synthesize closing tokens like PHP fallback.
-- Fixed native HTML processor paragraph breadcrumbs so block starts synthesize an implied `
` while inline starts remain inside the paragraph.
-- Fixed native HTML processor heading breadcrumbs so heading starts close an open heading instead of nesting inside it.
-- Fixed native HTML processor button breadcrumbs so nested button starts and explicit button closers synthesize intervening closing tokens like PHP fallback.
-- Fixed native HTML processor summary breadcrumbs so `` synthesizes pending `` closers like PHP fallback.
-- Fixed native HTML processor form breadcrumbs so duplicate form starts are ignored like PHP fallback.
-- Fixed native HTML processor nobr breadcrumbs so nested starts and explicit closers synthesize intervening closing tokens like PHP fallback.
-- Fixed native HTML processor select breadcrumbs so input starts close open option/select ancestors like PHP fallback.
-- Fixed native HTML processor select breadcrumbs so hr starts close open option ancestors like PHP fallback.
-- Fixed native HTML processor select breadcrumbs so textarea starts close open option/select ancestors like PHP fallback.
-- Fixed native HTML processor fragment breadcrumbs so explicit html/body starts are ignored like PHP fallback.
-- Fixed native HTML processor table-form breadcrumbs so table child starts and text close pending forms, while explicit `` stops table parsing like PHP fallback.
-- Fixed native HTML processor table breadcrumbs so selected unsupported child starts abort table parsing like PHP fallback, while comments and script-like children after table forms remain outside the form.
-- Fixed native HTML processor table section breadcrumbs so repeated sections and captions after rows or colgroups close intervening table ancestors like PHP fallback.
-- Fixed native HTML processor table breadcrumbs so stray closing tags are ignored instead of clearing open table ancestry.
-- Fixed native HTML processor table breadcrumbs so unsupported stray closing tags abort table parsing like PHP fallback.
-- Fixed native HTML processor text tokens so raw null bytes are omitted, line breaks are normalized, leading raw or character-reference whitespace is subdivided, and unsupported non-whitespace table text stops parsing like PHP fallback.
-- Enabled public native-backed HTML fragments for omitted-paragraph cases while keeping table fragments and full-document parsing on PHP fallback.
-- Enabled public native-backed HTML fragments for list, description-list, select/option/optgroup, and ruby implied-token cases while keeping tables, omitted paragraphs, and full-document parsing on PHP fallback.
-- Fixed native HTML invalid closing tags so underscore and colon starts expose funky-comment metadata like PHP fallback.
-- Fixed native HTML unusual attribute names so punctuation starts such as `.x` and `@x` are preserved like PHP fallback.
-- Fixed native HTML equals-start attribute names so malformed attributes such as `=b` and `a/=c` are preserved like PHP fallback.
-- Fixed native HTML boolean attribute removals so serialization preserves surrounding spacing like PHP fallback.
-- Fixed native HTML comment closing so `--!>` closes comments and unclosed comments do not expose tokens, matching PHP fallback.
-- Fixed public native-backed XML parse exceptions so `get_exception()` reports native syntax errors like PHP fallback.
-- Fixed native XML streaming processors so incomplete input can pause, accept appended bytes, and resume token scanning.
-- Fixed native XML processors so documents without a document element report a syntax error after token scanning.
-- Fixed native XML processors so leading whitespace outside the document element is skipped like PHP fallback.
-- Fixed native XML processors so opening tag `get_modifiable_text()` exposes decoded inner source like PHP fallback.
-- Optimized native HTML prefixed-attribute name scanners to avoid lowercase string allocations when deduplicating matches.
-- Added native HTML tag processor string-cast support for direct native processor instances.
-- Added native HTML self-closing flag accessors for direct native tag and processor instances.
-- Added native HTML namespace accessors for direct native tag and processor instances.
-- Added native HTML qualified-name accessors for direct native tag processor instances.
-- Added native HTML processor qualified-name accessors for direct native processor instances.
-- Added native HTML class-list and class-membership accessors for direct native tag and processor instances.
-- Added native HTML complete-input pause status for direct native tag processor instances.
-- Added native HTML processor complete-input pause status for direct native processor instances.
-- Added native HTML processor prefixed-attribute count access for direct native processor instances.
-- Added native HTML processor prefixed-attribute aggregate summaries for direct native processor instances.
-- Added native HTML processor tag-prefix summary batches for direct native processor instances.
-- Added native HTML processor compact tag-prefix summary batch aliases for direct native processor instances.
-- Added native HTML processor compact tag-prefix count batches for direct native processor instances.
-- Added native HTML processor tag-inventory aggregate summaries for direct native processor instances.
-- Added native HTML processor prefixed-attribute removal support for direct native processor instances.
-- Added native HTML processor document-level prefixed-attribute removal support for direct native processor instances.
-- Added native HTML processor updated HTML serialization for direct native processor instances.
-- Added native HTML processor string-cast serialization for direct native processor instances.
-- Added native HTML processor normalization/serialization through the PHP fragment serializer, text subdivision, modifiable text mutation, DOCTYPE info access, and processor full-parser/stepping support for direct native HTML instances.
-- Added native-default HTML conformance coverage proving public normalization and serialization keep PHP serializer semantics when the native extension is loaded.
-- Added native-default HTML conformance coverage proving inherited public processor aggregate scans complete after native remaining-document summaries.
-- Added native-default HTML conformance coverage proving inherited public processor tag batches complete after final short native batches.
-- Added native-default HTML conformance coverage proving direct compact tag batches complete after final short native batches.
-- Added native-default HTML and XML conformance coverage proving partially advanced document-level prefixed-attribute removals finish public processors.
-- Added native-default HTML conformance coverage proving partially advanced remaining-document aggregate summaries finish public tag and inherited processor wrappers.
-- Fixed public HTML native-default parser state after native token and token-summary advancement so started processors still reject serialization.
-- Fixed public HTML processor native-default token-summary batches so full and final short native batches leave public processors complete, matching PHP fallback exhaustion.
-- Fixed public HTML tag-processor native-default state and text-edit delegation after native token/tag advancement.
-- Fixed public HTML tag summary batch native-default state so current-tag helpers remain available after batch advancement.
-- Fixed public HTML tag-prefix count batch native-default state so current-tag helpers remain available after count-only batch advancement.
-- Fixed public HTML tag-processor native-default batches so final short native tag, tag-prefix, matching-tag, and count batches leave public processors complete, matching PHP fallback exhaustion.
-- Fixed public HTML tag aggregate native-default state so remaining-document summaries leave processors complete.
-- Added native HTML processor public token-summary batch rows for direct native processor instances.
-- Added native HTML processor public tag-summary batch rows for direct native processor instances.
-- Added native HTML processor compact tag-summary batch alias support for direct native processor instances.
-- Added native HTML processor compact matching-tag summary batch alias support for direct native processor instances.
-- Added native HTML processor compact matching-tag attribute summary batch alias support for direct native processor instances.
-- Added native HTML processor compact matching-tag multi-attribute summary batch alias support for direct native processor instances.
-- Added native HTML processor public matching-tag summary batch rows for direct native processor instances.
-- Added native HTML processor public matching-tag attribute summary batch rows for direct native processor instances.
-- Added native HTML processor public matching-tag multi-attribute summary batch rows for direct native processor instances.
-- Added native HTML processor matching-tag attribute aggregate summaries for direct native processor instances.
-- Added native XML streaming factory support for direct native processor instances.
-- Added native XML reentrancy cursor support for direct native processor instances.
-- Fixed public XML native-default content and import inventory summaries so remaining-document native scans leave processors finished.
-- Fixed public XML native-default structural inventory summaries so remaining-document native scans leave processors finished.
-- Fixed public XML native-default metadata and payload inventory summaries so remaining-document native scans leave processors finished.
-- Fixed public XML native-default token, tag, matching-tag, prefixed-attribute, and document-removal aggregate summaries so remaining-document native scans leave processors finished.
-- Fixed public XML native-default token, tag, matching-tag, and count batches so exhausted native scans leave processors finished.
-- Added public XML native-default compact batch conformance coverage for token, tag, count, matching-tag, and matching-tag count exhaustion.
-- Added native HTML public tag-summary batch rows for direct native tag processor instances.
-- Added native HTML public matching-tag summary batch rows for direct native tag processor instances.
-- Added native HTML public matching-tag attribute summary batch rows for direct native tag processor instances.
-- Added native HTML public matching-tag multi-attribute summary batch rows for direct native tag processor instances.
-- Added native HTML compact tag-prefix summary batch alias support for direct native tag processor instances.
-- Added native HTML processor tag-name access for direct native processor instances.
-- Added native HTML processor prefixed-attribute name access for direct native processor instances.
-- Added native HTML processor attribute-removal support for direct native processor instances.
-- Added native HTML processor virtual-token status support for direct native processor instances.
-- Added native HTML processor last-error diagnostics for direct native processor instances.
-- Added native HTML processor unsupported-exception diagnostics for direct native processor instances.
-- Added native HTML processor closer-expectation status for direct native processor instances.
-- Added native HTML processor static void-element checks for direct native processor instances.
-- Added native HTML processor static special-category checks for direct native processor instances.
-- Added native HTML processor breadcrumb matching for direct native processor instances.
-- Added native HTML bookmark lifecycle support for direct native tag and processor instances.
-- Added native XML read-only serialization methods for `get_updated_xml()` and string casts on direct native processor instances.
-- Added native XML `is_finished()` support for direct native processor exhaustion checks.
-- Added native XML `get_exception()` support for direct native processor diagnostics.
-- Added native XML complete-input status methods for direct native processor instances.
-- Added native XML complete-input append rejection for direct native processor instances.
-- Added native XML tag-opener and closer-expectation status methods for direct native processor instances.
-- Added native XML token byte-offset support for direct native processor instances.
-- Added native XML public token-summary batch rows for direct native processor instances.
-- Added native XML public tag-summary batch rows for direct native processor instances.
-- Added native XML public matching-tag summary batch rows for direct native processor instances.
-- Added native XML breadcrumb matching support for direct native processor instances.
-- Added native XML bookmark lifecycle support for direct native processor instances.
-- Added native XML compact tag-count batch aliases for direct native processor instances.
-- Added native XML DOCTYPE name support for direct native processor instances.
-- Added native XML DOCTYPE SYSTEM and PUBLIC literal support for direct native processor instances.
-- Added explicit read-only mutation method results for direct native XML processor instances.
-- Added an XML content inventory summary API with a native Rust-backed source-scan path for combined attribute-value and payload-byte audits.
-- Added an XML import inventory summary API with a native Rust-backed source-scan path for combined structure, attribute, and payload audits.
-- Fixed native-backed public HTML processors so attribute and class mutations are applied by the native delegate before serialization.
-- Fixed native HTML attribute mutation composition so removing a newly-added attribute cancels the pending insertion.
-- Fixed native HTML class mutation composition so read-after-remove and remove-then-add behavior matches PHP fallback semantics.
-- Fixed native HTML boolean attribute reads so parsed and newly-set boolean attributes return `true`.
-- Fixed native XML prefixed namespace declarations with empty values so they reject before exposing a token like the PHP fallback.
diff --git a/README.md b/README.md
index ff9c9ba0a..33778599a 100644
--- a/README.md
+++ b/README.md
@@ -7,16 +7,6 @@
Standalone, dependency-free PHP libraries for use in WordPress plugins and standalone PHP projects.
-### Experimental Native APIs extension
-
-The toolkit now publishes an experimental Native APIs extension for performance-sensitive `WP_HTML_Tag_Processor`, `XMLProcessor`, and `URLInTextProcessor` workloads. Public PHP APIs keep their pure-PHP fallback behavior when the extension is absent, incompatible, or disabled with `WP_NATIVE_APIS_DISABLE_DEFAULTS`.
-
-- [Native APIs docs](https://wordpress.github.io/php-toolkit/native-apis.html) explain which APIs are accelerated and how the fallback model works.
-- [Compile the host PHP extension](https://wordpress.github.io/php-toolkit/native-php-extension.html) when you want to benchmark the Rust-backed implementation locally.
-- [Test the latest PHP.wasm extension in WordPress Playground](https://playground.wordpress.net/?php=8.5&php-extension=https%3A%2F%2Fwordpress.github.io%2Fphp-toolkit%2Fwp_native_apis-wasm-extension%2Flatest%2Fmanifest.json&blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2FWordPress%2Fphp-toolkit%2Ftrunk%2Fextensions%2Fnative-apis%2Fplayground%2Fblueprint.json).
-- [Browse WASM releases for Playground](https://wordpress.github.io/php-toolkit/wp_native_apis-wasm-extension/) for manifests, checksums, PHP 8.0–8.5 test links, and benchmark summaries.
-- [Read the extension README](extensions/native-apis/README.md) for lower-level design notes and release checklists.
-
### Components
| Component | What it does | Live docs |
diff --git a/bin/benchmark-native-apis.php b/bin/benchmark-native-apis.php
deleted file mode 100644
index fb22b34d7..000000000
--- a/bin/benchmark-native-apis.php
+++ /dev/null
@@ -1,4473 +0,0 @@
- 25,
- 'component' => 'all',
- 'mode' => 'php',
- 'required' => false,
- 'name' => null,
- 'disable_native_defaults' => false,
- );
-
- foreach ( array_slice( $argv, 1 ) as $arg ) {
- if ( 0 === strpos( $arg, '--iterations=' ) ) {
- $options['iterations'] = max( 1, (int) substr( $arg, strlen( '--iterations=' ) ) );
- continue;
- }
-
- if ( 0 === strpos( $arg, '--component=' ) ) {
- $options['component'] = strtolower( substr( $arg, strlen( '--component=' ) ) );
- continue;
- }
-
- if ( 0 === strpos( $arg, '--mode=' ) ) {
- $options['mode'] = strtolower( substr( $arg, strlen( '--mode=' ) ) );
- continue;
- }
-
- if ( 0 === strpos( $arg, '--name=' ) ) {
- $options['name'] = substr( $arg, strlen( '--name=' ) );
- continue;
- }
-
- if ( '--help' === $arg || '-h' === $arg ) {
- fwrite(
- STDOUT,
- "Usage: php bin/benchmark-native-apis.php [--iterations=25] [--component=all|html|xml|url] [--mode=php|native|both] [--name=workload] [--disable-native-defaults] [--require-native]\n"
- );
- exit( 0 );
- }
-
- if ( '--disable-native-defaults' === $arg ) {
- $options['disable_native_defaults'] = true;
- continue;
- }
-
- if ( '--require-native' === $arg ) {
- $options['required'] = 'native';
- continue;
- }
- }
-
- if ( ! in_array( $options['component'], array( 'all', 'html', 'xml', 'url' ), true ) ) {
- fwrite( STDERR, "Invalid --component value. Expected all, html, xml, or url.\n" );
- exit( 1 );
- }
-
- if ( ! in_array( $options['mode'], array( 'php', 'native', 'both' ), true ) ) {
- fwrite( STDERR, "Invalid --mode value. Expected php, native, or both.\n" );
- exit( 1 );
- }
-
- return $options;
-}
-
-/**
- * Fail the benchmark when a required implementation row is unavailable.
- *
- * PHP-only development environments intentionally emit unavailable native rows
- * without failing. Release and post-build benchmarking jobs should pass
- * --require-native so missing extension classes fail loudly.
- *
- * @param array $results Benchmark result rows.
- * @param string|bool $required Required implementation mode.
- */
-function wp_toolkit_native_api_benchmark_maybe_fail_for_missing_results( $results, $required ) {
- if ( false === $required ) {
- return;
- }
-
- $missing = array();
- foreach ( $results as $result ) {
- if ( $required !== $result['implementation'] || ! empty( $result['available'] ) ) {
- continue;
- }
-
- $missing[] = sprintf(
- '%s (%s): %s',
- $result['name'],
- $result['implementation'],
- isset( $result['message'] ) ? $result['message'] : 'implementation is unavailable'
- );
- }
-
- if ( ! $missing ) {
- return;
- }
-
- fwrite(
- STDERR,
- "Required benchmark implementations are unavailable:\n - " . implode( "\n - ", $missing ) . "\n"
- );
- exit( 1 );
-}
-
-/**
- * Checks whether the selected implementation mode should run.
- *
- * @param string $selected_mode Selected benchmark mode.
- * @param string $target_mode Candidate implementation mode.
- * @return bool Whether to run the implementation.
- */
-function wp_toolkit_native_api_benchmark_should_run( $selected_mode, $target_mode ) {
- return 'both' === $selected_mode || $selected_mode === $target_mode;
-}
-
-/**
- * Run a benchmark workload.
- *
- * @param string $name Workload name.
- * @param string $mode Implementation mode.
- * @param int $iterations Iteration count.
- * @param string $class_name Expected class name.
- * @param callable $callback Workload callback.
- * @return array
- */
-function wp_toolkit_native_api_benchmark_run( $name, $mode, $iterations, $class_name, $callback ) {
- if (
- isset( $GLOBALS['wp_toolkit_native_api_benchmark_name_filter'] ) &&
- null !== $GLOBALS['wp_toolkit_native_api_benchmark_name_filter'] &&
- $name !== $GLOBALS['wp_toolkit_native_api_benchmark_name_filter']
- ) {
- return null;
- }
-
- if ( ! class_exists( $class_name ) ) {
- return array(
- 'name' => $name,
- 'implementation' => $mode,
- 'class' => $class_name,
- 'available' => false,
- 'message' => "Class {$class_name} is not available.",
- );
- }
-
- gc_collect_cycles();
- $start_memory = memory_get_usage( true );
- $start_peak = memory_get_peak_usage( true );
- $start_wall = microtime( true );
- $start_cpu = wp_toolkit_native_api_benchmark_cpu_time();
- $operations = 0;
-
- try {
- for ( $i = 0; $i < $iterations; $i++ ) {
- $operations += call_user_func( $callback );
- }
- } catch ( Throwable $exception ) {
- return array(
- 'name' => $name,
- 'implementation' => $mode,
- 'class' => $class_name,
- 'available' => false,
- 'message' => $exception->getMessage(),
- );
- }
-
- $end_cpu = wp_toolkit_native_api_benchmark_cpu_time();
- $wall = microtime( true ) - $start_wall;
- $cpu = null === $start_cpu || null === $end_cpu ? null : $end_cpu - $start_cpu;
- $peak = memory_get_peak_usage( true );
-
- return array(
- 'name' => $name,
- 'implementation' => $mode,
- 'class' => $class_name,
- 'available' => true,
- 'iterations' => $iterations,
- 'operations' => $operations,
- 'wall_seconds' => round( $wall, 6 ),
- 'cpu_seconds' => null === $cpu ? null : round( $cpu, 6 ),
- 'peak_memory' => $peak,
- 'peak_memory_delta' => max( 0, $peak - $start_peak ),
- 'memory_delta' => memory_get_usage( true ) - $start_memory,
- );
-}
-
-/**
- * Get process CPU time when available.
- *
- * @return float|null
- */
-function wp_toolkit_native_api_benchmark_cpu_time() {
- if ( ! function_exists( 'getrusage' ) ) {
- return null;
- }
-
- $usage = getrusage();
- if ( ! isset( $usage['ru_utime.tv_sec'], $usage['ru_utime.tv_usec'], $usage['ru_stime.tv_sec'], $usage['ru_stime.tv_usec'] ) ) {
- return null;
- }
-
- return (float) $usage['ru_utime.tv_sec']
- + ( $usage['ru_utime.tv_usec'] / 1000000 )
- + (float) $usage['ru_stime.tv_sec']
- + ( $usage['ru_stime.tv_usec'] / 1000000 );
-}
-
-/**
- * Benchmark the lower-level HTML tag processor.
- *
- * @return int Number of tags visited.
- */
-function wp_toolkit_native_api_benchmark_html_tags() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $tag_count = 0;
- $attr_count = 0;
-
- while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
- ++$tag_count;
- $names = $processor->get_attribute_names_with_prefix( 'data-' );
- if ( is_array( $names ) ) {
- $attr_count += count( $names );
- }
- }
-
- return $tag_count + $attr_count;
-}
-
-/**
- * Benchmark the lower-level HTML tag processor with prefix-count reads.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_html_tag_prefix_count() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $tag_count = 0;
- $attr_count = 0;
-
- while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
- ++$tag_count;
- $count = $processor->count_attribute_names_with_prefix( 'data-' );
- if ( is_int( $count ) ) {
- $attr_count += $count;
- }
- }
-
- return $tag_count + $attr_count;
-}
-
-/**
- * Benchmark the lower-level HTML tag processor with chunked tag summaries.
- *
- * @return int Number of tags visited.
- */
-function wp_toolkit_native_api_benchmark_html_tag_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $count = 0;
-
- do {
- $batch = $processor->next_tag_compact_summary_batch( 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark the lower-level HTML tag processor with chunked tag-name summaries.
- *
- * @return int Number of matching tags visited.
- */
-function wp_toolkit_native_api_benchmark_html_matching_tag_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $count = 0;
-
- do {
- $batch = $processor->next_matching_tag_compact_summary_batch( 'a', 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark the lower-level HTML tag processor with chunked tag-name and attribute summaries.
- *
- * @return int Number of matching tags visited plus attribute bytes consumed.
- */
-function wp_toolkit_native_api_benchmark_html_matching_tag_attribute_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $count = 0;
-
- do {
- $batch = $processor->next_matching_tag_attribute_compact_summary_batch( 'a', 'href', 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_attribute_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark the lower-level HTML tag processor with chunked tag-name and multi-attribute summaries.
- *
- * @return int Number of matching tags visited plus attribute bytes consumed.
- */
-function wp_toolkit_native_api_benchmark_html_matching_tag_attributes_batch() {
- $html = wp_toolkit_native_api_benchmark_html_link_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $count = 0;
-
- do {
- $batch = $processor->next_matching_tag_attributes_compact_summary_batch( 'a', array( 'href', 'title', 'rel' ), 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_attributes_batch( $batch, 3 );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark a fused lower-level HTML link-audit summary.
- *
- * @return int Number of matching tags plus attributes and attribute bytes consumed.
- */
-function wp_toolkit_native_api_benchmark_html_link_audit_summary() {
- $html = wp_toolkit_native_api_benchmark_html_link_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_matching_tag_attributes( 'a', array( 'href', 'title', 'rel' ), true );
-
- if ( ! is_array( $summary ) || ! isset( $summary['tag_count'], $summary['attribute_count'], $summary['attribute_value_bytes'] ) ) {
- throw new RuntimeException( 'HTML link audit summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['attribute_count'] + (int) $summary['attribute_value_bytes'];
-}
-
-/**
- * Benchmark a fused lower-level HTML tag inventory summary.
- *
- * @return int Number of tags plus attributes and unique tag names counted.
- */
-function wp_toolkit_native_api_benchmark_html_tag_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_tag_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['attribute_count'], $summary['unique_tag_name_count'] )
- ) {
- throw new RuntimeException( 'HTML tag inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['attribute_count'] + (int) $summary['unique_tag_name_count'];
-}
-
-/**
- * Benchmark a fused lower-level HTML heading inventory summary.
- *
- * @return int Number of tags plus headings counted.
- */
-function wp_toolkit_native_api_benchmark_html_heading_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_heading_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['heading_count'], $summary['h2_count'] )
- ) {
- throw new RuntimeException( 'HTML heading inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['heading_count'] + (int) $summary['h2_count'];
-}
-
-/**
- * Benchmark a fused lower-level HTML ID inventory summary.
- *
- * @return int Number of tags plus ID counts and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_id_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_id_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_id_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['id_tag_count'], $summary['duplicate_id_count'], $summary['id_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML ID inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['id_tag_count'] + (int) $summary['duplicate_id_count'] + (int) $summary['id_value_bytes'];
-}
-
-/**
- * Benchmark a fused lower-level HTML attribute inventory summary.
- *
- * @return int Number of tags plus attributes and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_attribute_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_attribute_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['attribute_count'], $summary['attribute_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML attribute inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['attribute_count'] + (int) $summary['attribute_value_bytes'];
-}
-
-/**
- * Benchmark a fused lower-level HTML data-attribute inventory summary.
- *
- * @return int Number of tags plus data attributes and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_data_attribute_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_data_attribute_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['data_attribute_count'], $summary['data_attribute_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML data-attribute inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['data_attribute_count'] + (int) $summary['data_attribute_value_bytes'];
-}
-
-/**
- * Benchmark a fused lower-level HTML ARIA attribute inventory summary.
- *
- * @return int Number of tags plus ARIA attributes and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_aria_attribute_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_aria_attribute_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['aria_attribute_count'], $summary['aria_attribute_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML ARIA attribute inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['aria_attribute_count'] + (int) $summary['aria_attribute_value_bytes'];
-}
-
-/**
- * Benchmark a fused lower-level HTML class inventory summary.
- *
- * @return int Number of tags plus class names and class value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_class_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_class_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['class_name_count'], $summary['class_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML class inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['class_name_count'] + (int) $summary['class_value_bytes'];
-}
-
-/**
- * Benchmark a fused lower-level HTML resource inventory summary.
- *
- * @return int Number of tags plus resource attributes and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_resource_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_resource_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['resource_attribute_count'], $summary['resource_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML resource inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['resource_attribute_count'] + (int) $summary['resource_value_bytes'];
-}
-
-/**
- * Benchmark a fused lower-level HTML image inventory summary.
- *
- * @return int Number of tags plus images, attributes, and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_image_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_image_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_image_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['image_count'], $summary['src_value_bytes'], $summary['alt_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML image inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['image_count'] + (int) $summary['src_value_bytes'] + (int) $summary['alt_value_bytes'];
-}
-
-/**
- * Benchmark a fused lower-level HTML script inventory summary.
- *
- * @return int Number of tags plus scripts, attributes, and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_script_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_script_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_script_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['script_count'], $summary['inline_script_bytes'], $summary['src_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML script inventory summary benchmark returned an invalid summary.' );
- }
-
- return (
- (int) $summary['tag_count'] +
- (int) $summary['script_count'] +
- (int) $summary['inline_script_bytes'] +
- (int) $summary['src_value_bytes']
- );
-}
-
-/**
- * Benchmark a fused lower-level HTML form inventory summary.
- *
- * @return int Number of tags plus controls and name bytes counted.
- */
-function wp_toolkit_native_api_benchmark_html_form_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_form_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_form_inventory( true );
-
- if (
- ! is_array( $summary ) ||
- ! isset( $summary['tag_count'], $summary['control_count'], $summary['control_name_value_bytes'] )
- ) {
- throw new RuntimeException( 'HTML form inventory summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['control_count'] + (int) $summary['control_name_value_bytes'];
-}
-
-/**
- * Benchmark the lower-level HTML tag processor with chunked prefix summaries.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_html_tag_prefix_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $count = 0;
-
- do {
- $batch = $processor->next_tag_prefix_compact_summary_batch( 'data-', 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_prefix_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark the lower-level HTML tag processor with chunked prefix-count summaries.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_html_tag_prefix_count_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $count = 0;
-
- do {
- $summary = $processor->next_tag_prefix_count_compact_batch( 'data-', 256, true );
- if ( is_string( $summary ) && '' !== $summary ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_prefix_count_batch( $summary );
- }
- } while ( is_string( $summary ) && '' !== $summary );
-
- return $count;
-}
-
-/**
- * Benchmark a fused lower-level HTML prefix-count summary.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_html_tag_prefix_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->summarize_attribute_names_with_prefix( 'data-', true );
-
- if ( ! is_array( $summary ) || ! isset( $summary['tag_count'], $summary['attribute_count'] ) ) {
- throw new RuntimeException( 'HTML tag prefix summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['attribute_count'];
-}
-
-/**
- * Benchmark the native lower-level HTML tag processor.
- *
- * @return int Number of tags visited.
- */
-function wp_toolkit_native_api_benchmark_native_html_tags() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $has_strings = method_exists( $processor, 'get_attribute_names_with_prefix_string' );
- $tag_count = 0;
- $attr_count = 0;
-
- while ( $processor->next_tag_any( true, 1 ) ) {
- ++$tag_count;
- if ( $has_strings ) {
- $names = $processor->get_attribute_names_with_prefix_string( 'data-' );
- if ( is_string( $names ) && '' !== $names ) {
- $attr_count += substr_count( $names, "\x1f" ) + 1;
- }
- } else {
- $names = $processor->get_attribute_names_with_prefix( 'data-' );
- if ( is_array( $names ) ) {
- $attr_count += count( $names );
- }
- }
- }
-
- return $tag_count + $attr_count;
-}
-
-/**
- * Benchmark the native lower-level HTML tag processor with prefix-count reads.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_tag_prefix_count() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $tag_count = 0;
- $attr_count = 0;
-
- while ( $processor->next_tag_any( true, 1 ) ) {
- ++$tag_count;
- $count = $processor->count_attribute_names_with_prefix( 'data-' );
- if ( is_int( $count ) ) {
- $attr_count += $count;
- }
- }
-
- return $tag_count + $attr_count;
-}
-
-/**
- * Benchmark the native lower-level HTML tag processor with chunked tag summaries.
- *
- * @return int Number of tags visited.
- */
-function wp_toolkit_native_api_benchmark_native_html_tag_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $count = 0;
-
- if ( method_exists( $processor, 'next_tag_compact_summary_batch' ) ) {
- do {
- $batch = $processor->next_tag_compact_summary_batch( 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
- }
-
- while ( $processor->next_tag_any( true, 1 ) ) {
- ++$count;
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native lower-level HTML tag processor with chunked tag-name summaries.
- *
- * @return int Number of matching tags visited.
- */
-function wp_toolkit_native_api_benchmark_native_html_matching_tag_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $count = 0;
-
- if ( method_exists( $processor, 'next_matching_tag_compact_summary_batch' ) ) {
- do {
- $batch = $processor->next_matching_tag_compact_summary_batch( 'a', 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
- }
-
- while ( $processor->next_tag_any( true, 1 ) ) {
- if ( 'A' === $processor->get_tag() ) {
- ++$count;
- }
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native lower-level HTML tag processor with chunked tag-name and attribute summaries.
- *
- * @return int Number of matching tags visited plus attribute bytes consumed.
- */
-function wp_toolkit_native_api_benchmark_native_html_matching_tag_attribute_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $count = 0;
-
- if ( method_exists( $processor, 'next_matching_tag_attribute_compact_summary_batch' ) ) {
- do {
- $batch = $processor->next_matching_tag_attribute_compact_summary_batch( 'a', 'href', 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_attribute_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
- }
-
- while ( $processor->next_tag_any( true, 1 ) ) {
- if ( 'A' === $processor->get_tag() ) {
- ++$count;
- $value = $processor->get_attribute( 'href' );
- if ( is_string( $value ) ) {
- $count += strlen( $value );
- }
- }
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native lower-level HTML tag processor with chunked tag-name and multi-attribute summaries.
- *
- * @return int Number of matching tags visited plus attribute bytes consumed.
- */
-function wp_toolkit_native_api_benchmark_native_html_matching_tag_attributes_batch() {
- $html = wp_toolkit_native_api_benchmark_html_link_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $count = 0;
-
- if ( method_exists( $processor, 'next_matching_tag_attributes_compact_summary_batch' ) ) {
- do {
- $batch = $processor->next_matching_tag_attributes_compact_summary_batch( 'a', "href\x1ftitle\x1frel", 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_attributes_batch( $batch, 3 );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
- }
-
- while ( $processor->next_tag_any( true, 1 ) ) {
- if ( 'A' === $processor->get_tag() ) {
- ++$count;
- foreach ( array( 'href', 'title', 'rel' ) as $attribute_name ) {
- $value = $processor->get_attribute( $attribute_name );
- if ( is_string( $value ) ) {
- $count += strlen( $value );
- }
- }
- }
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native lower-level HTML link-audit summary.
- *
- * @return int Number of matching tags plus attributes and attribute bytes consumed.
- */
-function wp_toolkit_native_api_benchmark_native_html_link_audit_summary() {
- $html = wp_toolkit_native_api_benchmark_html_link_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( method_exists( $processor, 'summarize_matching_tag_attributes' ) ) {
- $summary = $processor->summarize_matching_tag_attributes( 'a', "href\x1ftitle\x1frel", true );
- if ( is_string( $summary ) ) {
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 === count( $parts ) ) {
- return (int) $parts[0] + (int) $parts[1] + (int) $parts[2];
- }
- }
- }
-
- return wp_toolkit_native_api_benchmark_native_html_matching_tag_attributes_batch();
-}
-
-/**
- * Benchmark the native lower-level HTML tag inventory summary.
- *
- * @return int Number of tags plus attributes and unique tag names counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_tag_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_tag_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_tag_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML tag inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 5 );
- if ( 5 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML tag inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[3] + (int) $parts[4];
-}
-
-/**
- * Benchmark the native lower-level HTML heading inventory summary.
- *
- * @return int Number of tags plus headings counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_heading_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_heading_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_heading_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML heading inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 8 );
- if ( 8 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML heading inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[1] + (int) $parts[3];
-}
-
-/**
- * Benchmark the native lower-level HTML ID inventory summary.
- *
- * @return int Number of tags plus ID counts and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_id_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_id_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_id_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_id_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML ID inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 5 );
- if ( 5 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML ID inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[1] + (int) $parts[3] + (int) $parts[4];
-}
-
-/**
- * Benchmark the native lower-level HTML attribute inventory summary.
- *
- * @return int Number of tags plus attributes and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_attribute_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_attribute_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_attribute_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML attribute inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 4 );
- if ( 4 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML attribute inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[1] + (int) $parts[3];
-}
-
-/**
- * Benchmark the native lower-level HTML data-attribute inventory summary.
- *
- * @return int Number of tags plus data attributes and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_data_attribute_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_data_attribute_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_data_attribute_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML data-attribute inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 5 );
- if ( 5 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML data-attribute inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[2] + (int) $parts[4];
-}
-
-/**
- * Benchmark the native lower-level HTML ARIA attribute inventory summary.
- *
- * @return int Number of tags plus ARIA attributes and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_aria_attribute_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_aria_attribute_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_aria_attribute_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML ARIA attribute inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 5 );
- if ( 5 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML ARIA attribute inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[2] + (int) $parts[4];
-}
-
-/**
- * Benchmark the native lower-level HTML class inventory summary.
- *
- * @return int Number of tags plus class names and class value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_class_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_class_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_class_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML class inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 5 );
- if ( 5 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML class inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[2] + (int) $parts[4];
-}
-
-/**
- * Benchmark the native lower-level HTML resource inventory summary.
- *
- * @return int Number of tags plus resource attributes and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_resource_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_resource_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_resource_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML resource inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 5 );
- if ( 5 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML resource inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[2] + (int) $parts[4];
-}
-
-/**
- * Benchmark the native lower-level HTML image inventory summary.
- *
- * @return int Number of tags plus images, attributes, and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_image_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_image_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_image_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_image_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML image inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 8 );
- if ( 8 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML image inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[1] + (int) $parts[6] + (int) $parts[7];
-}
-
-/**
- * Benchmark the native lower-level HTML script inventory summary.
- *
- * @return int Number of tags plus scripts, attributes, and value bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_script_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_script_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_script_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_script_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML script inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 8 );
- if ( 8 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML script inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[1] + (int) $parts[6] + (int) $parts[7];
-}
-
-/**
- * Benchmark the native lower-level HTML form inventory summary.
- *
- * @return int Number of tags plus controls and name bytes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_form_inventory_summary() {
- $html = wp_toolkit_native_api_benchmark_html_form_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( ! method_exists( $processor, 'summarize_form_inventory' ) ) {
- return 0;
- }
-
- $summary = $processor->summarize_form_inventory( true );
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native HTML form inventory summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- if ( 6 !== count( $parts ) ) {
- throw new RuntimeException( 'Native HTML form inventory summary benchmark returned an invalid compact row.' );
- }
-
- return (int) $parts[0] + (int) $parts[2] + (int) $parts[5];
-}
-
-/**
- * Benchmark the native lower-level HTML tag processor with chunked prefix summaries.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_tag_prefix_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $count = 0;
-
- if ( method_exists( $processor, 'next_tag_prefix_summary_batch' ) ) {
- do {
- $batch = $processor->next_tag_prefix_summary_batch( 'data-', 256, true );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_prefix_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
- }
-
- return wp_toolkit_native_api_benchmark_native_html_tag_prefix_count();
-}
-
-/**
- * Benchmark the native lower-level HTML tag processor with chunked prefix-count summaries.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_tag_prefix_count_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $count = 0;
-
- if ( ! method_exists( $processor, 'next_tag_prefix_count_compact_batch' ) ) {
- return wp_toolkit_native_api_benchmark_native_html_tag_prefix_batch();
- }
-
- do {
- $summary = $processor->next_tag_prefix_count_compact_batch( 'data-', 256, true );
- if ( is_string( $summary ) && '' !== $summary ) {
- $count += wp_toolkit_native_api_benchmark_count_html_tag_prefix_count_batch( $summary );
- }
- } while ( is_string( $summary ) && '' !== $summary );
-
- return $count;
-}
-
-/**
- * Counts compact HTML tag batch rows without allocating per-row arrays.
- *
- * @param string $batch Compact tag summary batch.
- * @return int Number of tags visited.
- */
-function wp_toolkit_native_api_benchmark_count_html_tag_batch( $batch ) {
- $count = 0;
- $offset = 0;
- $length = strlen( $batch );
-
- while ( $offset < $length ) {
- $row_end = strpos( $batch, "\x1e", $offset );
- if ( false === $row_end ) {
- $row_end = $length;
- }
-
- $first = strpos( $batch, "\x1f", $offset );
- if ( false === $first || $first >= $row_end ) {
- throw new RuntimeException( 'HTML tag batch benchmark returned an invalid summary row.' );
- }
-
- ++$count;
- $offset = $row_end + 1;
- }
-
- return $count;
-}
-
-/**
- * Counts compact HTML tag-attribute batch rows without allocating per-row arrays.
- *
- * @param string $batch Compact tag-attribute summary batch.
- * @return int Number of tags visited plus attribute bytes consumed.
- */
-function wp_toolkit_native_api_benchmark_count_html_tag_attribute_batch( $batch ) {
- $count = 0;
- $offset = 0;
- $length = strlen( $batch );
-
- while ( $offset < $length ) {
- $row_end = strpos( $batch, "\x1e", $offset );
- if ( false === $row_end ) {
- $row_end = $length;
- }
-
- $first = strpos( $batch, "\x1f", $offset );
- if ( false === $first || $first >= $row_end ) {
- throw new RuntimeException( 'HTML tag attribute batch benchmark returned an invalid summary row.' );
- }
-
- $second = strpos( $batch, "\x1f", $first + 1 );
- if ( false === $second || $second >= $row_end ) {
- throw new RuntimeException( 'HTML tag attribute batch benchmark returned an invalid summary row.' );
- }
-
- ++$count;
- if ( '1' === substr( $batch, $second + 1, 1 ) ) {
- $count += $row_end - $second - 2;
- }
- $offset = $row_end + 1;
- }
-
- return $count;
-}
-
-/**
- * Counts compact HTML tag multi-attribute batch rows without allocating per-row arrays.
- *
- * @param string $batch Compact tag-attribute summary batch.
- * @param int $attribute_count Number of attribute fields per row.
- * @return int Number of tags visited plus attribute bytes consumed.
- */
-function wp_toolkit_native_api_benchmark_count_html_tag_attributes_batch( $batch, $attribute_count ) {
- $count = 0;
- $offset = 0;
- $length = strlen( $batch );
-
- while ( $offset < $length ) {
- $row_end = strpos( $batch, "\x1e", $offset );
- if ( false === $row_end ) {
- $row_end = $length;
- }
-
- $field_start = $offset;
- for ( $field_index = 0; $field_index < 2 + $attribute_count; ++$field_index ) {
- $field_end = strpos( $batch, "\x1f", $field_start );
- if ( false === $field_end || $field_end > $row_end ) {
- $field_end = $row_end;
- }
-
- if ( $field_start > $row_end || ( $field_index < 1 + $attribute_count && $field_end >= $row_end ) ) {
- throw new RuntimeException( 'HTML tag attributes batch benchmark returned an invalid summary row.' );
- }
-
- if ( $field_index >= 2 && '1' === substr( $batch, $field_start, 1 ) ) {
- $count += $field_end - $field_start - 1;
- }
-
- $field_start = $field_end + 1;
- }
-
- ++$count;
- $offset = $row_end + 1;
- }
-
- return $count;
-}
-
-/**
- * Counts compact HTML tag-prefix batch rows without allocating per-row arrays.
- *
- * @param string $batch Compact tag-prefix summary batch.
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_count_html_tag_prefix_batch( $batch ) {
- $count = 0;
- $offset = 0;
- $length = strlen( $batch );
-
- while ( $offset < $length ) {
- $row_end = strpos( $batch, "\x1e", $offset );
- if ( false === $row_end ) {
- $row_end = $length;
- }
-
- $first = strpos( $batch, "\x1f", $offset );
- if ( false === $first || $first >= $row_end ) {
- throw new RuntimeException( 'HTML tag prefix batch benchmark returned an invalid summary row.' );
- }
-
- $second = strpos( $batch, "\x1f", $first + 1 );
- if ( false === $second || $second >= $row_end ) {
- throw new RuntimeException( 'HTML tag prefix batch benchmark returned an invalid summary row.' );
- }
-
- ++$count;
- $count += (int) substr( $batch, $second + 1, $row_end - $second - 1 );
- $offset = $row_end + 1;
- }
-
- return $count;
-}
-
-/**
- * Counts a compact HTML tag-prefix count batch.
- *
- * @param string $summary Compact tag and attribute count summary.
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_count_html_tag_prefix_count_batch( $summary ) {
- $parts = explode( "\x1f", $summary, 2 );
- if ( 2 !== count( $parts ) ) {
- throw new RuntimeException( 'HTML tag prefix count batch benchmark returned an invalid summary row.' );
- }
-
- return (int) $parts[0] + (int) $parts[1];
-}
-
-/**
- * Benchmark the native lower-level HTML prefix-count summary.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_html_tag_prefix_summary() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
-
- if ( method_exists( $processor, 'summarize_attribute_names_with_prefix' ) ) {
- $summary = $processor->summarize_attribute_names_with_prefix( 'data-', true );
- if ( is_string( $summary ) ) {
- $parts = explode( "\x1f", $summary, 2 );
- if ( 2 === count( $parts ) ) {
- return (int) $parts[0] + (int) $parts[1];
- }
- }
- }
-
- return wp_toolkit_native_api_benchmark_native_html_tag_prefix_count();
-}
-
-/**
- * Benchmark an HTML prefix-scan and remove-attribute sanitization workflow.
- *
- * @return int Number of tags visited plus attributes removed.
- */
-function wp_toolkit_native_api_benchmark_html_tag_sanitizer() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Tag_Processor( $html );
- $summary = $processor->remove_attributes_with_prefix_from_document( 'data-', true );
-
- if ( ! is_array( $summary ) || ! isset( $summary['tag_count'], $summary['removed_count'], $summary['html'] ) ) {
- throw new RuntimeException( 'HTML tag sanitizer benchmark returned an invalid summary.' );
- }
-
- $updated_html = $summary['html'];
- if ( false !== strpos( $updated_html, 'data-' ) ) {
- throw new RuntimeException( 'HTML tag sanitizer benchmark left data-* attributes in the output.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['removed_count'];
-}
-
-/**
- * Benchmark the native lower-level HTML prefix-scan and remove-attribute path.
- *
- * @return int Number of tags visited plus attributes removed.
- */
-function wp_toolkit_native_api_benchmark_native_html_tag_sanitizer() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = new WP_HTML_Native_Tag_Processor( $html );
- $tag_count = 0;
- $removed_count = 0;
-
- if ( method_exists( $processor, 'remove_attributes_with_prefix_from_document' ) ) {
- $summary = $processor->remove_attributes_with_prefix_from_document( 'data-', true );
- if ( is_string( $summary ) ) {
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 === count( $parts ) ) {
- if ( false !== strpos( $parts[2], 'data-' ) ) {
- throw new RuntimeException( 'Native HTML tag sanitizer benchmark left data-* attributes in the output.' );
- }
-
- return (int) $parts[0] + (int) $parts[1];
- }
- }
- }
-
- while ( $processor->next_tag_any( true, 1 ) ) {
- ++$tag_count;
- if ( method_exists( $processor, 'remove_attributes_with_prefix' ) ) {
- $count = $processor->remove_attributes_with_prefix( 'data-' );
- if ( is_int( $count ) ) {
- $removed_count += $count;
- }
- continue;
- }
-
- $names = $processor->get_attribute_names_with_prefix( 'data-' );
- if ( ! is_array( $names ) ) {
- continue;
- }
-
- foreach ( $names as $name ) {
- if ( $processor->remove_attribute( $name ) ) {
- ++$removed_count;
- }
- }
- }
-
- $updated_html = $processor->get_updated_html();
- if ( false !== strpos( $updated_html, 'data-' ) ) {
- throw new RuntimeException( 'Native HTML tag sanitizer benchmark left data-* attributes in the output.' );
- }
-
- return $tag_count + $removed_count;
-}
-
-/**
- * Benchmark the full HTML processor.
- *
- * @return int Number of tokens visited.
- */
-function wp_toolkit_native_api_benchmark_html_processor() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = WP_HTML_Processor::create_fragment( $html );
- $count = 0;
-
- while ( $processor->next_token() ) {
- ++$count;
- $processor->get_token_type();
- $processor->get_token_name();
- $processor->get_breadcrumbs();
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native full HTML processor.
- *
- * @return int Number of tokens visited.
- */
-function wp_toolkit_native_api_benchmark_native_html_processor() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = WP_HTML_Native_Processor::create_fragment( $html );
- $count = 0;
-
- while ( $processor->next_token() ) {
- ++$count;
- $processor->get_token_type();
- $processor->get_token_name();
- $processor->get_breadcrumbs();
- }
-
- return $count;
-}
-
-/**
- * Benchmark public HTML processor token summary batches.
- *
- * @return int Number of tokens visited.
- */
-function wp_toolkit_native_api_benchmark_html_token_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = WP_HTML_Processor::create_fragment( $html );
- $count = 0;
-
- do {
- $batch = $processor->next_token_compact_summary_batch( 256 );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_token_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark native HTML processor token summary batches.
- *
- * @return int Number of tokens visited.
- */
-function wp_toolkit_native_api_benchmark_native_html_token_batch() {
- $html = wp_toolkit_native_api_benchmark_html_document();
- $processor = WP_HTML_Native_Processor::create_fragment( $html );
- $count = 0;
-
- do {
- $batch = $processor->next_token_compact_summary_batch( 256 );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_html_token_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Counts compact HTML token batch rows without allocating per-row arrays.
- *
- * @param string $batch Compact token summary batch.
- * @return int Number of tokens visited.
- */
-function wp_toolkit_native_api_benchmark_count_html_token_batch( $batch ) {
- $count = 0;
- $offset = 0;
- $length = strlen( $batch );
-
- while ( $offset < $length ) {
- $row_end = strpos( $batch, "\x1e", $offset );
- if ( false === $row_end ) {
- $row_end = $length;
- }
-
- $first = strpos( $batch, "\x1f", $offset );
- if ( false === $first || $first >= $row_end ) {
- throw new RuntimeException( 'HTML token batch benchmark returned an invalid summary row.' );
- }
-
- ++$count;
- $offset = $row_end + 1;
- }
-
- return $count;
-}
-
-/**
- * Benchmark the public URL-in-text processor.
- *
- * @return int Number of URLs visited.
- */
-function wp_toolkit_native_api_benchmark_url_in_text_processor() {
- $text = wp_toolkit_native_api_benchmark_url_in_text_document();
- $processor = new URLInTextProcessor( $text, 'https://example.com' );
- $count = 0;
-
- while ( $processor->next_url() ) {
- ++$count;
- $raw_url = $processor->get_raw_url();
- $parsed_url = $processor->get_parsed_url();
- if ( false === $raw_url || false === $parsed_url ) {
- throw new RuntimeException( 'URLInTextProcessor benchmark exposed an invalid URL row.' );
- }
- }
-
- if ( 360 !== $count ) {
- throw new RuntimeException( "URLInTextProcessor benchmark expected 360 URLs, found {$count}." );
- }
-
- return $count;
-}
-
-/**
- * Benchmark the direct native URL-in-text processor.
- *
- * @return int Number of URLs visited.
- */
-function wp_toolkit_native_api_benchmark_native_url_in_text_processor() {
- $text = wp_toolkit_native_api_benchmark_url_in_text_document();
- $native_class = 'WordPress\\DataLiberation\\URL\\NativeURLInTextProcessor';
- $processor = new $native_class( $text, 'https://example.com' );
- $count = 0;
- $total_url_bytes = 0;
-
- while ( $processor->next_url() ) {
- ++$count;
- $raw_url = $processor->get_raw_url();
- if ( ! is_string( $raw_url ) || '' === $raw_url ) {
- throw new RuntimeException( 'Native URL-in-text benchmark exposed an invalid URL row.' );
- }
- $total_url_bytes += $processor->get_url_length();
- $processor->get_url_starts_at();
- $processor->had_protocol();
- }
-
- if ( 360 !== $count || 0 === $total_url_bytes ) {
- throw new RuntimeException( "Native URL-in-text benchmark expected 360 URLs, found {$count}." );
- }
-
- return $count;
-}
-
-/**
- * Benchmark the XML processor.
- *
- * @return int Number of tokens visited.
- */
-function wp_toolkit_native_api_benchmark_xml_processor() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
- $count = 0;
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- while ( $processor->next_token() ) {
- ++$count;
- $processor->get_token_type();
- $processor->get_token_name();
- if ( null !== $processor->get_tag_local_name() ) {
- $processor->get_tag_namespace_and_local_name();
- $processor->get_attribute( '', 'id' );
- }
- }
-
- return $count;
-}
-
-/**
- * Benchmark a fused public XML token summary.
- *
- * @return int Number of tokens visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_token_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_token_stream( 'id' );
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['attribute_count'] ) ) {
- throw new RuntimeException( 'XML token summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['token_count'] + (int) $summary['attribute_count'];
-}
-
-/**
- * Benchmark a fused public XML document inventory summary.
- *
- * @return int Number of inventoried tokens and structural markers.
- */
-function wp_toolkit_native_api_benchmark_xml_document_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_inventory_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_document_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['tag_count'], $summary['closing_tag_count'], $summary['max_depth'] ) ) {
- throw new RuntimeException( 'XML document inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 3 !== (int) $summary['max_depth'] ||
- 0 === (int) $summary['comment_count'] ||
- 0 === (int) $summary['cdata_count']
- ) {
- throw new RuntimeException( 'XML document inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['tag_count'] +
- (int) $summary['closing_tag_count'] +
- (int) $summary['text_token_count'] +
- (int) $summary['comment_count'] +
- (int) $summary['cdata_count'] +
- (int) $summary['empty_element_count']
- );
-}
-
-/**
- * Benchmark a fused public XML element inventory summary.
- *
- * @return int Number of inventoried element-name markers.
- */
-function wp_toolkit_native_api_benchmark_xml_element_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_element_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_element_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['tag_count'], $summary['unique_tag_name_count'], $summary['duplicate_tag_name_count'] ) ) {
- throw new RuntimeException( 'XML element inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 7 !== (int) $summary['unique_tag_name_count'] ||
- 0 === (int) $summary['duplicate_tag_name_count'] ||
- 0 === (int) $summary['namespaced_tag_count'] ||
- 0 === (int) $summary['empty_element_count']
- ) {
- throw new RuntimeException( 'XML element inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['tag_count'] +
- (int) $summary['closing_tag_count'] +
- (int) $summary['unique_tag_name_count'] +
- (int) $summary['duplicate_tag_name_count'] +
- (int) $summary['namespaced_tag_count'] +
- (int) $summary['empty_element_count']
- );
-}
-
-/**
- * Benchmark a fused public XML depth inventory summary.
- *
- * @return int Number of inventoried tags and depth markers.
- */
-function wp_toolkit_native_api_benchmark_xml_depth_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_depth_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_depth_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['tag_count'], $summary['total_tag_depth'], $summary['max_depth'] ) ) {
- throw new RuntimeException( 'XML depth inventory benchmark returned an invalid summary.' );
- }
-
- if (
- (int) $summary['max_depth'] < 4 ||
- 0 === (int) $summary['empty_element_count'] ||
- 0 === (int) $summary['nested_tag_count'] ||
- 0 === (int) $summary['total_tag_depth']
- ) {
- throw new RuntimeException( 'XML depth inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['tag_count'] +
- (int) $summary['closing_tag_count'] +
- (int) $summary['empty_element_count'] +
- (int) $summary['root_level_tag_count'] +
- (int) $summary['nested_tag_count'] +
- (int) $summary['total_tag_depth']
- );
-}
-
-/**
- * Benchmark a fused public XML leaf inventory summary.
- *
- * @return int Number of inventoried tags and leaf/branch markers.
- */
-function wp_toolkit_native_api_benchmark_xml_leaf_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_depth_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_leaf_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['leaf_element_count'], $summary['branch_element_count'] ) ) {
- throw new RuntimeException( 'XML leaf inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['empty_element_count'] ||
- 0 === (int) $summary['leaf_element_count'] ||
- 0 === (int) $summary['branch_element_count'] ||
- (int) $summary['max_child_element_count'] < 2
- ) {
- throw new RuntimeException( 'XML leaf inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['tag_count'] +
- (int) $summary['closing_tag_count'] +
- (int) $summary['empty_element_count'] +
- (int) $summary['leaf_element_count'] +
- (int) $summary['branch_element_count'] +
- (int) $summary['max_child_element_count']
- );
-}
-
-/**
- * Benchmark a fused public XML structural inventory summary.
- *
- * @return int Number of inventoried structural markers.
- */
-function wp_toolkit_native_api_benchmark_xml_structural_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_depth_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_structural_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['unique_tag_name_count'], $summary['total_tag_depth'], $summary['leaf_element_count'] ) ) {
- throw new RuntimeException( 'XML structural inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['empty_element_count'] ||
- 0 === (int) $summary['nested_tag_count'] ||
- 0 === (int) $summary['total_tag_depth'] ||
- 0 === (int) $summary['leaf_element_count'] ||
- 0 === (int) $summary['branch_element_count'] ||
- (int) $summary['max_child_element_count'] < 2
- ) {
- throw new RuntimeException( 'XML structural inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['tag_count'] +
- (int) $summary['closing_tag_count'] +
- (int) $summary['unique_tag_name_count'] +
- (int) $summary['duplicate_tag_name_count'] +
- (int) $summary['namespaced_tag_count'] +
- (int) $summary['empty_element_count'] +
- (int) $summary['root_level_tag_count'] +
- (int) $summary['nested_tag_count'] +
- (int) $summary['total_tag_depth'] +
- (int) $summary['leaf_element_count'] +
- (int) $summary['branch_element_count'] +
- (int) $summary['max_child_element_count']
- );
-}
-
-/**
- * Benchmark a fused public XML attribute inventory summary.
- *
- * @return int Number of inventoried tokens and attributes.
- */
-function wp_toolkit_native_api_benchmark_xml_attribute_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_attribute_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_attribute_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['attribute_count'], $summary['max_attribute_count'] ) ) {
- throw new RuntimeException( 'XML attribute inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['attribute_count'] ||
- 0 === (int) $summary['namespaced_attribute_count'] ||
- 0 === (int) $summary['tags_with_attributes_count'] ||
- (int) $summary['max_attribute_count'] < 3
- ) {
- throw new RuntimeException( 'XML attribute inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['attribute_count'] +
- (int) $summary['namespaced_attribute_count'] +
- (int) $summary['tags_with_attributes_count'] +
- (int) $summary['max_attribute_count']
- );
-}
-
-/**
- * Benchmark a fused public XML ID inventory summary.
- *
- * @return int Number of inventoried tokens and IDs.
- */
-function wp_toolkit_native_api_benchmark_xml_id_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_id_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_id_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['id_attribute_count'], $summary['duplicate_id_count'] ) ) {
- throw new RuntimeException( 'XML ID inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['id_attribute_count'] ||
- 0 === (int) $summary['unique_id_count'] ||
- 0 === (int) $summary['duplicate_id_count'] ||
- 0 === (int) $summary['id_value_bytes']
- ) {
- throw new RuntimeException( 'XML ID inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['id_attribute_count'] +
- (int) $summary['unique_id_count'] +
- (int) $summary['duplicate_id_count'] +
- (int) $summary['id_value_bytes']
- );
-}
-
-/**
- * Benchmark a fused public XML namespace inventory summary.
- *
- * @return int Number of inventoried namespace-related tokens and attributes.
- */
-function wp_toolkit_native_api_benchmark_xml_namespace_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_namespace_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_namespace_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['namespaced_tag_count'], $summary['unique_namespace_count'] ) ) {
- throw new RuntimeException( 'XML namespace inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['namespaced_tag_count'] ||
- 0 === (int) $summary['namespaced_attribute_count'] ||
- (int) $summary['unique_namespace_count'] < 2
- ) {
- throw new RuntimeException( 'XML namespace inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['namespaced_tag_count'] +
- (int) $summary['attribute_count'] +
- (int) $summary['namespaced_attribute_count'] +
- (int) $summary['unique_namespace_count']
- );
-}
-
-/**
- * Benchmark a fused public XML text inventory summary.
- *
- * @return int Number of inventoried text tokens and bytes.
- */
-function wp_toolkit_native_api_benchmark_xml_text_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_text_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_text_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['text_token_count'], $summary['total_text_bytes'] ) ) {
- throw new RuntimeException( 'XML text inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['text_token_count'] ||
- 0 === (int) $summary['cdata_count'] ||
- 0 === (int) $summary['non_empty_text_count'] ||
- 0 === (int) $summary['whitespace_text_count'] ||
- (int) $summary['max_text_bytes'] < 10
- ) {
- throw new RuntimeException( 'XML text inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['text_token_count'] +
- (int) $summary['cdata_count'] +
- (int) $summary['non_empty_text_count'] +
- (int) $summary['whitespace_text_count'] +
- (int) $summary['total_text_bytes']
- );
-}
-
-/**
- * Benchmark a fused public XML processing instruction inventory summary.
- *
- * @return int Number of inventoried processing instruction tokens and bytes.
- */
-function wp_toolkit_native_api_benchmark_xml_processing_instruction_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_processing_instruction_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_processing_instruction_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['processing_instruction_count'], $summary['total_instruction_bytes'] ) ) {
- throw new RuntimeException( 'XML processing instruction inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['processing_instruction_count'] ||
- 0 === (int) $summary['xml_declaration_count'] ||
- 0 === (int) $summary['non_empty_instruction_count'] ||
- (int) $summary['max_instruction_bytes'] < 20
- ) {
- throw new RuntimeException( 'XML processing instruction inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['processing_instruction_count'] +
- (int) $summary['xml_declaration_count'] +
- (int) $summary['non_empty_instruction_count'] +
- (int) $summary['total_instruction_bytes']
- );
-}
-
-/**
- * Benchmark a fused public XML comment inventory summary.
- *
- * @return int Number of inventoried comment tokens and bytes.
- */
-function wp_toolkit_native_api_benchmark_xml_comment_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_comment_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_comment_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['comment_count'], $summary['total_comment_bytes'] ) ) {
- throw new RuntimeException( 'XML comment inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['comment_count'] ||
- 0 === (int) $summary['non_empty_comment_count'] ||
- 0 === (int) $summary['empty_comment_count'] ||
- (int) $summary['max_comment_bytes'] < 20
- ) {
- throw new RuntimeException( 'XML comment inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['comment_count'] +
- (int) $summary['non_empty_comment_count'] +
- (int) $summary['empty_comment_count'] +
- (int) $summary['total_comment_bytes']
- );
-}
-
-/**
- * Benchmark a fused public XML payload inventory summary.
- *
- * @return int Number of inventoried payload tokens and bytes.
- */
-function wp_toolkit_native_api_benchmark_xml_payload_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_payload_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_payload_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['text_token_count'], $summary['total_payload_bytes'] ) ) {
- throw new RuntimeException( 'XML payload inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['text_token_count'] ||
- 0 === (int) $summary['cdata_count'] ||
- 0 === (int) $summary['comment_count'] ||
- 0 === (int) $summary['processing_instruction_count'] ||
- (int) $summary['max_payload_bytes'] < 20
- ) {
- throw new RuntimeException( 'XML payload inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['text_token_count'] +
- (int) $summary['cdata_count'] +
- (int) $summary['comment_count'] +
- (int) $summary['processing_instruction_count'] +
- (int) $summary['total_payload_bytes']
- );
-}
-
-/**
- * Benchmark a fused public XML content inventory summary.
- *
- * @return int Number of inventoried content tokens, attributes, and bytes.
- */
-function wp_toolkit_native_api_benchmark_xml_content_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_payload_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_content_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['attribute_count'], $summary['total_payload_bytes'] ) ) {
- throw new RuntimeException( 'XML content inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['attribute_count'] ||
- 0 === (int) $summary['text_token_count'] ||
- 0 === (int) $summary['cdata_count'] ||
- 0 === (int) $summary['comment_count'] ||
- 0 === (int) $summary['processing_instruction_count'] ||
- (int) $summary['max_attribute_value_bytes'] < 2 ||
- (int) $summary['max_payload_bytes'] < 20
- ) {
- throw new RuntimeException( 'XML content inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['tag_count'] +
- (int) $summary['attribute_count'] +
- (int) $summary['text_token_count'] +
- (int) $summary['cdata_count'] +
- (int) $summary['comment_count'] +
- (int) $summary['processing_instruction_count'] +
- (int) $summary['total_attribute_value_bytes'] +
- (int) $summary['total_payload_bytes']
- );
-}
-
-/**
- * Benchmark a fused public XML import inventory summary.
- *
- * @return int Number of inventoried structure, content, and byte counts.
- */
-function wp_toolkit_native_api_benchmark_xml_import_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_payload_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_import_inventory();
- if ( ! is_array( $summary ) || ! isset( $summary['token_count'], $summary['attribute_count'], $summary['total_payload_bytes'] ) ) {
- throw new RuntimeException( 'XML import inventory benchmark returned an invalid summary.' );
- }
-
- if (
- 0 === (int) $summary['tag_count'] ||
- 0 === (int) $summary['leaf_element_count'] ||
- 0 === (int) $summary['attribute_count'] ||
- 0 === (int) $summary['text_token_count'] ||
- 0 === (int) $summary['cdata_count'] ||
- 0 === (int) $summary['comment_count'] ||
- 0 === (int) $summary['processing_instruction_count'] ||
- (int) $summary['max_payload_bytes'] < 20
- ) {
- throw new RuntimeException( 'XML import inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $summary['token_count'] +
- (int) $summary['tag_count'] +
- (int) $summary['closing_tag_count'] +
- (int) $summary['unique_tag_name_count'] +
- (int) $summary['leaf_element_count'] +
- (int) $summary['branch_element_count'] +
- (int) $summary['attribute_count'] +
- (int) $summary['text_token_count'] +
- (int) $summary['cdata_count'] +
- (int) $summary['comment_count'] +
- (int) $summary['processing_instruction_count'] +
- (int) $summary['total_attribute_value_bytes'] +
- (int) $summary['total_payload_bytes']
- );
-}
-
-/**
- * Benchmark a public XML token summary batch loop.
- *
- * @return int Number of tokens visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_token_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
- $count = 0;
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- do {
- $batch = $processor->next_token_compact_summary_batch( 256 );
- if ( is_string( $batch ) && '' !== $batch ) {
- foreach ( explode( "\x1e", $batch ) as $row ) {
- $parts = explode( "\x1f", $row, 6 );
- if ( 6 !== count( $parts ) ) {
- throw new RuntimeException( 'XML token batch benchmark returned an invalid summary row.' );
- }
-
- ++$count;
- if ( isset( $parts[5][0] ) && '1' === $parts[5][0] ) {
- ++$count;
- }
- }
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark a fused public XML tag summary.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_tag_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_tag_stream( 'id' );
- if ( ! is_array( $summary ) || ! isset( $summary['tag_count'], $summary['attribute_count'] ) ) {
- throw new RuntimeException( 'XML tag summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['attribute_count'];
-}
-
-/**
- * Benchmark a public XML tag summary batch loop.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_tag_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
- $count = 0;
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- do {
- $batch = $processor->next_tag_compact_summary_batch( 256, 'id' );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_xml_tag_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark a public XML matching tag summary batch loop.
- *
- * @return int Number of matching tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_matching_tag_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
- $count = 0;
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- do {
- $batch = $processor->next_matching_tag_compact_summary_batch( 256, 'https://wordpress.org', 'item', 'id' );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_xml_tag_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- return $count;
-}
-
-/**
- * Benchmark a public XML matching tag count batch loop.
- *
- * @return int Number of matching tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_matching_tag_count_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
- $count = 0;
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- do {
- $summary = $processor->next_matching_tag_count_compact_batch( 256, 'https://wordpress.org', 'item', 'id' );
- if ( is_string( $summary ) ) {
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'XML matching tag count batch benchmark returned an invalid summary row.' );
- }
-
- $count += (int) $parts[1] + (int) $parts[2];
- }
- } while ( is_string( $summary ) );
-
- return $count;
-}
-
-/**
- * Benchmark a document-level XML matching tag summary.
- *
- * @return int Number of matching tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_matching_tag_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_matching_tag_stream( 'https://wordpress.org', 'item', 'id' );
- if ( ! is_array( $summary ) || ! isset( $summary['tag_count'], $summary['attribute_count'] ) ) {
- throw new RuntimeException( 'XML matching tag summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['attribute_count'];
-}
-
-/**
- * Benchmark a document-level XML matching tag multi-attribute summary.
- *
- * @return int Number of matching tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_matching_tag_attributes_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_matching_attribute_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_matching_tag_attributes_stream(
- 'https://wordpress.org',
- 'item',
- array( 'id', 'slug', 'status' )
- );
- if ( ! is_array( $summary ) || ! isset( $summary['tag_count'], $summary['attribute_count'] ) ) {
- throw new RuntimeException( 'XML matching tag attributes summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['attribute_count'];
-}
-
-/**
- * Benchmark a public XML tag count batch loop.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_tag_count_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $processor = XMLProcessor::create_from_string( $xml );
- $count = 0;
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- do {
- $summary = $processor->next_tag_count_compact_batch( 256, 'id' );
- if ( is_string( $summary ) ) {
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'XML tag count batch benchmark returned an invalid summary row.' );
- }
-
- $count += (int) $parts[1] + (int) $parts[2];
- }
- } while ( is_string( $summary ) );
-
- return $count;
-}
-
-/**
- * Benchmark a document-level XML attribute-prefix summary.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_xml_prefix_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_attribute_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->summarize_attribute_names_with_prefix( null, 'data-' );
- if ( ! is_array( $summary ) || ! isset( $summary['tag_count'], $summary['attribute_count'] ) ) {
- throw new RuntimeException( 'XML prefix summary benchmark returned an invalid summary.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['attribute_count'];
-}
-
-/**
- * Benchmark document-level XML prefixed-attribute removal.
- *
- * @return int Number of tags visited plus matching attributes removed.
- */
-function wp_toolkit_native_api_benchmark_xml_prefix_sanitizer() {
- $xml = wp_toolkit_native_api_benchmark_xml_attribute_document();
- $processor = XMLProcessor::create_from_string( $xml );
-
- if ( false === $processor ) {
- throw new RuntimeException( 'XMLProcessor could not parse benchmark document.' );
- }
-
- $summary = $processor->remove_attributes_with_prefix_from_document( null, 'data-' );
- if ( ! is_array( $summary ) || ! isset( $summary['tag_count'], $summary['removed_count'], $summary['xml'] ) ) {
- throw new RuntimeException( 'XML prefix sanitizer benchmark returned an invalid summary.' );
- }
-
- if ( false !== strpos( $summary['xml'], ' data-' ) ) {
- throw new RuntimeException( 'XML prefix sanitizer benchmark left data-* attributes in the output.' );
- }
-
- return (int) $summary['tag_count'] + (int) $summary['removed_count'];
-}
-
-/**
- * Benchmark the native XML processor.
- *
- * @return int Number of tokens visited.
- */
-function wp_toolkit_native_api_benchmark_native_xml_processor() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $count = 0;
-
- while ( $processor->next_token() ) {
- ++$count;
- $processor->get_token_type();
- $processor->get_token_name();
- $processor->get_attribute( 'id' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native fused XML token summary.
- *
- * @return int Number of tokens visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_token_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_token_stream( 'id' );
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML token summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML token summary benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return (int) $parts[0] + (int) $parts[2];
-}
-
-/**
- * Benchmark native XML document inventory summary.
- *
- * @return int Number of inventoried tokens and structural markers.
- */
-function wp_toolkit_native_api_benchmark_native_xml_document_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_inventory_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_document_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML document inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 8 );
- if ( 8 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML document inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 3 !== (int) $parts[6] || 0 === (int) $parts[4] || 0 === (int) $parts[5] ) {
- throw new RuntimeException( 'Native XML document inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5] +
- (int) $parts[7]
- );
-}
-
-/**
- * Benchmark native XML element inventory summary.
- *
- * @return int Number of inventoried element-name markers.
- */
-function wp_toolkit_native_api_benchmark_native_xml_element_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_element_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_element_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML element inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 7 );
- if ( 7 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML element inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 7 !== (int) $parts[3] || 0 === (int) $parts[4] || 0 === (int) $parts[5] || 0 === (int) $parts[6] ) {
- throw new RuntimeException( 'Native XML element inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5] +
- (int) $parts[6]
- );
-}
-
-/**
- * Benchmark native XML depth inventory summary.
- *
- * @return int Number of inventoried tags and depth markers.
- */
-function wp_toolkit_native_api_benchmark_native_xml_depth_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_depth_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_depth_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML depth inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 8 );
- if ( 8 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML depth inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( (int) $parts[7] < 4 || 0 === (int) $parts[3] || 0 === (int) $parts[5] || 0 === (int) $parts[6] ) {
- throw new RuntimeException( 'Native XML depth inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5] +
- (int) $parts[6]
- );
-}
-
-/**
- * Benchmark native XML leaf inventory summary.
- *
- * @return int Number of inventoried tags and leaf/branch markers.
- */
-function wp_toolkit_native_api_benchmark_native_xml_leaf_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_depth_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_leaf_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML leaf inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 7 );
- if ( 7 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML leaf inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[3] || 0 === (int) $parts[4] || 0 === (int) $parts[5] || (int) $parts[6] < 2 ) {
- throw new RuntimeException( 'Native XML leaf inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5] +
- (int) $parts[6]
- );
-}
-
-/**
- * Benchmark native XML structural inventory summary.
- *
- * @return int Number of inventoried structural markers.
- */
-function wp_toolkit_native_api_benchmark_native_xml_structural_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_depth_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_structural_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML structural inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 14 );
- if ( 14 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML structural inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[6] || 0 === (int) $parts[8] || 0 === (int) $parts[9] || 0 === (int) $parts[11] || 0 === (int) $parts[12] || (int) $parts[13] < 2 ) {
- throw new RuntimeException( 'Native XML structural inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5] +
- (int) $parts[6] +
- (int) $parts[7] +
- (int) $parts[8] +
- (int) $parts[9] +
- (int) $parts[11] +
- (int) $parts[12] +
- (int) $parts[13]
- );
-}
-
-/**
- * Benchmark native XML attribute inventory summary.
- *
- * @return int Number of inventoried tokens and attributes.
- */
-function wp_toolkit_native_api_benchmark_native_xml_attribute_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_attribute_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_attribute_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML attribute inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- if ( 6 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML attribute inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[2] || 0 === (int) $parts[3] || 0 === (int) $parts[4] || (int) $parts[5] < 3 ) {
- throw new RuntimeException( 'Native XML attribute inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5]
- );
-}
-
-/**
- * Benchmark native XML ID inventory summary.
- *
- * @return int Number of inventoried tokens and IDs.
- */
-function wp_toolkit_native_api_benchmark_native_xml_id_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_id_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_id_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML ID inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- if ( 6 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML ID inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[2] || 0 === (int) $parts[3] || 0 === (int) $parts[4] || 0 === (int) $parts[5] ) {
- throw new RuntimeException( 'Native XML ID inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5]
- );
-}
-
-/**
- * Benchmark native XML namespace inventory summary.
- *
- * @return int Number of inventoried namespace-related tokens and attributes.
- */
-function wp_toolkit_native_api_benchmark_native_xml_namespace_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_namespace_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_namespace_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML namespace inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- if ( 6 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML namespace inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[2] || 0 === (int) $parts[4] || (int) $parts[5] < 2 ) {
- throw new RuntimeException( 'Native XML namespace inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5]
- );
-}
-
-/**
- * Benchmark native XML text inventory summary.
- *
- * @return int Number of inventoried text tokens and bytes.
- */
-function wp_toolkit_native_api_benchmark_native_xml_text_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_text_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_text_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML text inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 7 );
- if ( 7 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML text inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[1] || 0 === (int) $parts[2] || 0 === (int) $parts[3] || 0 === (int) $parts[4] || (int) $parts[6] < 10 ) {
- throw new RuntimeException( 'Native XML text inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5]
- );
-}
-
-/**
- * Benchmark native XML processing instruction inventory summary.
- *
- * @return int Number of inventoried processing instruction tokens and bytes.
- */
-function wp_toolkit_native_api_benchmark_native_xml_processing_instruction_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_processing_instruction_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_processing_instruction_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML processing instruction inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- if ( 6 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML processing instruction inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[1] || 0 === (int) $parts[2] || 0 === (int) $parts[3] || (int) $parts[5] < 20 ) {
- throw new RuntimeException( 'Native XML processing instruction inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4]
- );
-}
-
-/**
- * Benchmark native XML comment inventory summary.
- *
- * @return int Number of inventoried comment tokens and bytes.
- */
-function wp_toolkit_native_api_benchmark_native_xml_comment_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_comment_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_comment_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML comment inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- if ( 6 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML comment inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[1] || 0 === (int) $parts[2] || 0 === (int) $parts[3] || (int) $parts[5] < 20 ) {
- throw new RuntimeException( 'Native XML comment inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4]
- );
-}
-
-/**
- * Benchmark native XML payload inventory summary.
- *
- * @return int Number of inventoried payload tokens and bytes.
- */
-function wp_toolkit_native_api_benchmark_native_xml_payload_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_payload_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_payload_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML payload inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 7 );
- if ( 7 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML payload inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[1] || 0 === (int) $parts[2] || 0 === (int) $parts[3] || 0 === (int) $parts[4] || (int) $parts[6] < 20 ) {
- throw new RuntimeException( 'Native XML payload inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5]
- );
-}
-
-/**
- * Benchmark native XML content inventory summary.
- *
- * @return int Number of inventoried content tokens, attributes, and bytes.
- */
-function wp_toolkit_native_api_benchmark_native_xml_content_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_payload_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_content_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML content inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 11 );
- if ( 11 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML content inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[2] || 0 === (int) $parts[3] || 0 === (int) $parts[4] || 0 === (int) $parts[5] || 0 === (int) $parts[6] || (int) $parts[8] < 2 || (int) $parts[10] < 20 ) {
- throw new RuntimeException( 'Native XML content inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[4] +
- (int) $parts[5] +
- (int) $parts[6] +
- (int) $parts[7] +
- (int) $parts[9]
- );
-}
-
-/**
- * Benchmark native XML import inventory summary.
- *
- * @return int Number of inventoried structure, content, and byte counts.
- */
-function wp_toolkit_native_api_benchmark_native_xml_import_inventory_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_payload_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_import_inventory();
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML import inventory benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 23 );
- if ( 23 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML import inventory benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- if ( 0 === (int) $parts[1] || 0 === (int) $parts[11] || 0 === (int) $parts[14] || 0 === (int) $parts[15] || 0 === (int) $parts[16] || 0 === (int) $parts[17] || 0 === (int) $parts[18] || (int) $parts[22] < 20 ) {
- throw new RuntimeException( 'Native XML import inventory benchmark returned unexpected counts.' );
- }
-
- return (
- (int) $parts[0] +
- (int) $parts[1] +
- (int) $parts[2] +
- (int) $parts[3] +
- (int) $parts[11] +
- (int) $parts[12] +
- (int) $parts[14] +
- (int) $parts[15] +
- (int) $parts[16] +
- (int) $parts[17] +
- (int) $parts[18] +
- (int) $parts[19] +
- (int) $parts[21]
- );
-}
-
-/**
- * Benchmark native XML token summary batches.
- *
- * @return int Number of tokens visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_token_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $count = 0;
-
- do {
- $batch = $processor->next_token_compact_summary_batch( 256 );
- if ( is_string( $batch ) && '' !== $batch ) {
- foreach ( explode( "\x1e", $batch ) as $row ) {
- $parts = explode( "\x1f", $row, 6 );
- if ( 6 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML token batch benchmark returned an invalid summary row.' );
- }
-
- ++$count;
- if ( isset( $parts[5][0] ) && '1' === $parts[5][0] ) {
- ++$count;
- }
- }
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native fused XML tag summary.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_tag_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_tag_stream( 'id' );
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML tag summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML tag summary benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return (int) $parts[1] + (int) $parts[2];
-}
-
-/**
- * Benchmark native XML tag summary batches.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_tag_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $count = 0;
-
- do {
- $batch = $processor->next_tag_compact_summary_batch( 256, 'id' );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_xml_tag_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return $count;
-}
-
-/**
- * Benchmark native XML matching tag summary batches.
- *
- * @return int Number of matching tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_matching_tag_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $count = 0;
-
- do {
- $batch = $processor->next_matching_tag_compact_summary_batch( 256, 'https://wordpress.org', 'item', 'id' );
- if ( is_string( $batch ) && '' !== $batch ) {
- $count += wp_toolkit_native_api_benchmark_count_xml_tag_batch( $batch );
- }
- } while ( is_string( $batch ) && '' !== $batch );
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return $count;
-}
-
-/**
- * Benchmark native XML matching tag count batches.
- *
- * @return int Number of matching tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_matching_tag_count_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $count = 0;
-
- do {
- $summary = $processor->next_matching_tag_count_batch( 256, 'https://wordpress.org', 'item', 'id' );
- if ( is_string( $summary ) ) {
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML matching tag count batch benchmark returned an invalid summary row.' );
- }
-
- $count += (int) $parts[1] + (int) $parts[2];
- }
- } while ( is_string( $summary ) );
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native document-level XML matching tag summary.
- *
- * @return int Number of matching tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_matching_tag_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_matching_tag_stream( 'https://wordpress.org', 'item', 'id' );
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML matching tag summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML matching tag summary benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return (int) $parts[1] + (int) $parts[2];
-}
-
-/**
- * Benchmark the native document-level XML matching tag multi-attribute summary.
- *
- * @return int Number of matching tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_matching_tag_attributes_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_matching_attribute_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_matching_tag_attributes_stream( 'https://wordpress.org', 'item', "id\x1fslug\x1fstatus" );
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML matching tag attributes summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML matching tag attributes summary benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return (int) $parts[1] + (int) $parts[2];
-}
-
-/**
- * Benchmark native XML tag count batches.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_tag_count_batch() {
- $xml = wp_toolkit_native_api_benchmark_xml_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $count = 0;
-
- do {
- $summary = $processor->next_tag_count_batch( 256, 'id' );
- if ( is_string( $summary ) ) {
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML tag count batch benchmark returned an invalid summary row.' );
- }
-
- $count += (int) $parts[1] + (int) $parts[2];
- }
- } while ( is_string( $summary ) );
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return $count;
-}
-
-/**
- * Counts compact XML tag batch rows without allocating per-row arrays.
- *
- * @param string $batch Compact tag summary batch.
- * @return int Number of tags plus matching attributes.
- */
-function wp_toolkit_native_api_benchmark_count_xml_tag_batch( $batch ) {
- $count = 0;
- $offset = 0;
- $length = strlen( $batch );
-
- while ( $offset < $length ) {
- $row_end = strpos( $batch, "\x1e", $offset );
- if ( false === $row_end ) {
- $row_end = $length;
- }
-
- $first = strpos( $batch, "\x1f", $offset );
- if ( false === $first || $first >= $row_end ) {
- throw new RuntimeException( 'XML tag batch benchmark returned an invalid summary row.' );
- }
-
- $second = strpos( $batch, "\x1f", $first + 1 );
- if ( false === $second || $second >= $row_end ) {
- throw new RuntimeException( 'XML tag batch benchmark returned an invalid summary row.' );
- }
-
- $third = strpos( $batch, "\x1f", $second + 1 );
- if ( false === $third || $third >= $row_end ) {
- throw new RuntimeException( 'XML tag batch benchmark returned an invalid summary row.' );
- }
-
- $fourth = strpos( $batch, "\x1f", $third + 1 );
- if ( false === $fourth || $fourth >= $row_end ) {
- throw new RuntimeException( 'XML tag batch benchmark returned an invalid summary row.' );
- }
-
- $fifth = strpos( $batch, "\x1f", $fourth + 1 );
- if ( false === $fifth || $fifth >= $row_end ) {
- throw new RuntimeException( 'XML tag batch benchmark returned an invalid summary row.' );
- }
-
- ++$count;
- if ( isset( $batch[ $fifth + 1 ] ) && '1' === $batch[ $fifth + 1 ] ) {
- ++$count;
- }
-
- $offset = $row_end + 1;
- }
-
- return $count;
-}
-
-/**
- * Benchmark the native document-level XML attribute-prefix summary.
- *
- * @return int Number of tags visited plus matching attributes counted.
- */
-function wp_toolkit_native_api_benchmark_native_xml_prefix_summary() {
- $xml = wp_toolkit_native_api_benchmark_xml_attribute_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->summarize_attribute_names_with_prefix( null, 'data-' );
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML prefix summary benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML prefix summary benchmark returned an invalid summary row.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return (int) $parts[1] + (int) $parts[2];
-}
-
-/**
- * Benchmark native document-level XML prefixed-attribute removal.
- *
- * @return int Number of tags visited plus matching attributes removed.
- */
-function wp_toolkit_native_api_benchmark_native_xml_prefix_sanitizer() {
- $xml = wp_toolkit_native_api_benchmark_xml_attribute_document();
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
- $processor = $class_name::create_from_string( $xml );
- $summary = $processor->remove_attributes_with_prefix_from_document( null, 'data-' );
-
- if ( ! is_string( $summary ) ) {
- throw new RuntimeException( 'Native XML prefix sanitizer benchmark returned an invalid summary.' );
- }
-
- $parts = explode( "\x1f", $summary, 3 );
- if ( 3 !== count( $parts ) ) {
- throw new RuntimeException( 'Native XML prefix sanitizer benchmark returned an invalid summary row.' );
- }
-
- if ( false !== strpos( $parts[2], ' data-' ) ) {
- throw new RuntimeException( 'Native XML prefix sanitizer benchmark left data-* attributes in the output.' );
- }
-
- if ( null !== $processor->get_last_error() ) {
- throw new RuntimeException( 'NativeXMLProcessor reported benchmark parse error: ' . $processor->get_last_error() );
- }
-
- return (int) $parts[0] + (int) $parts[1];
-}
-
-/**
- * Build a representative HTML document.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_html_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Title %1$d Body link
',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative HTML document with ID attributes and duplicates.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_html_id_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Title %1$d Body link
',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative HTML document with link attributes for multi-attribute scans.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_html_link_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Post %1$d
Feed %1$d
',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative HTML document with forms and named controls.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_html_form_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- '',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative HTML document with image attributes.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_html_image_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Image %1$d ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative HTML document with script attributes and inline scripts.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_html_script_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- Benchmark fixture markup.
- ' ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build representative plain text containing URL references.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_url_in_text_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Post %1$d: https://example.com/posts/%1$d?preview=1 references example.org/docs/%1$d and //cdn.example.org/assets/app-%1$d.js.',
- $i
- );
- }
-
- return implode( ' ', $items );
-}
-
-/**
- * Build a representative XML document.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Title %1$d post-%1$d ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document for structure inventory scans.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_inventory_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Title %1$d ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with repeated element names.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_element_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Title %1$d ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with nested importer elements.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_depth_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- '',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with repeated matching tag attribute audits.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_matching_attribute_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Title %1$d post-%1$d ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with repeated prefixed attribute scans.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_attribute_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Title %1$d post-%1$d ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with ID-heavy importer data.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_id_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $duplicate_id = $i % 20;
- $items[] = sprintf(
- 'Title %1$d post-%1$d ',
- $i,
- $duplicate_id
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with namespace-heavy importer data.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_namespace_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- 'Title %1$d author-%1$d ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with text-heavy importer data.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_text_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- '- Title %1$d
Excerpt %1$d with & entity text inside]]> ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with importer processing instructions.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_processing_instruction_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- '- Title %1$d
',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with importer comments.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_comment_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- '- Title %1$d
',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
-
-/**
- * Build a representative XML document with mixed payload-bearing tokens.
- *
- * @return string
- */
-function wp_toolkit_native_api_benchmark_xml_payload_document() {
- $items = array();
- for ( $i = 0; $i < 120; $i++ ) {
- $items[] = sprintf(
- '- Title %1$d
Excerpt %1$d with & entity text inside]]> ',
- $i
- );
- }
-
- return '' . implode( '', $items ) . ' ';
-}
diff --git a/bin/build-reference.php b/bin/build-reference.php
index 3023d661d..74648ece5 100755
--- a/bin/build-reference.php
+++ b/bin/build-reference.php
@@ -36,7 +36,7 @@
}
require __DIR__ . '/../vendor/autoload.php';
-const ASSET_VERSION = '20260526-native-apis';
+const ASSET_VERSION = '20260807';
const ROOT = __DIR__ . '/..';
const COMPONENTS = ROOT . '/components';
const DOCS = ROOT . '/docs/reference';
@@ -81,7 +81,6 @@
Learn
Reference
- Native APIs
GitHub
diff --git a/bin/summarize-native-api-benchmark.php b/bin/summarize-native-api-benchmark.php
deleted file mode 100644
index 8dc4852e7..000000000
--- a/bin/summarize-native-api-benchmark.php
+++ /dev/null
@@ -1,185 +0,0 @@
- \n" );
- exit( 1 );
-}
-
-$input_path = $argv[1];
-$output_path = $argv[2];
-
-$contents = file_get_contents( $input_path );
-if ( false === $contents ) {
- fwrite( STDERR, "Unable to read benchmark JSON: {$input_path}\n" );
- exit( 1 );
-}
-
-$rows = json_decode( $contents, true );
-if ( ! is_array( $rows ) ) {
- fwrite( STDERR, "Invalid benchmark JSON: {$input_path}\n" );
- exit( 1 );
-}
-
-$by_name = array();
-foreach ( $rows as $row ) {
- if ( ! is_array( $row ) || empty( $row['available'] ) || ! isset( $row['name'], $row['implementation'] ) ) {
- continue;
- }
-
- $name = (string) $row['name'];
- $implementation = (string) $row['implementation'];
- if ( ! isset( $by_name[ $name ] ) ) {
- $by_name[ $name ] = array();
- }
- $by_name[ $name ][ $implementation ] = $row;
-}
-
-$comparisons = array();
-foreach ( $by_name as $name => $implementations ) {
- if ( ! isset( $implementations['php'], $implementations['native'] ) ) {
- continue;
- }
-
- $php_row = $implementations['php'];
- $native_row = $implementations['native'];
- $php_wall = isset( $php_row['wall_seconds'] ) ? (float) $php_row['wall_seconds'] : 0.0;
- $native_wall = isset( $native_row['wall_seconds'] ) ? (float) $native_row['wall_seconds'] : 0.0;
-
- if ( 0.0 >= $php_wall || 0.0 >= $native_wall ) {
- continue;
- }
-
- $comparisons[] = array(
- 'name' => $name,
- 'label' => wp_toolkit_native_api_benchmark_label( $name ),
- 'component' => wp_toolkit_native_api_benchmark_component( $name ),
- 'iterations' => isset( $php_row['iterations'] ) ? (int) $php_row['iterations'] : null,
- 'operations' => isset( $php_row['operations'] ) ? (int) $php_row['operations'] : null,
- 'phpWallSeconds' => round( $php_wall, 6 ),
- 'nativeWallSeconds' => round( $native_wall, 6 ),
- 'speedup' => round( $php_wall / $native_wall, 2 ),
- 'phpClass' => isset( $php_row['class'] ) ? (string) $php_row['class'] : '',
- 'nativeClass' => isset( $native_row['class'] ) ? (string) $native_row['class'] : '',
- );
-}
-
-usort(
- $comparisons,
- function ( $a, $b ) {
- if ( $a['speedup'] === $b['speedup'] ) {
- return strcmp( $a['name'], $b['name'] );
- }
- return $a['speedup'] < $b['speedup'] ? 1 : -1;
- }
-);
-
-$summary = array(
- 'generatedAt' => gmdate( 'c' ),
- 'command' => 'php -d extension=extensions/native-apis/target/release/libwp_native_apis.so bin/benchmark-native-apis.php --iterations=50 --mode=both --disable-native-defaults --require-native',
- 'count' => count( $comparisons ),
- 'top' => array_slice( $comparisons, 0, 8 ),
- 'comparisons' => $comparisons,
-);
-
-$json = json_encode( $summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
-if ( false === $json || false === file_put_contents( $output_path, $json . "\n" ) ) {
- fwrite( STDERR, "Unable to write benchmark summary: {$output_path}\n" );
- exit( 1 );
-}
-
-/**
- * Convert a workload name into a short public label.
- *
- * @param string $name Workload name.
- * @return string Human-friendly label.
- */
-function wp_toolkit_native_api_benchmark_label( $name ) {
- $labels = array(
- 'html-tag-processor' => 'HTML tag scan',
- 'html-tag-prefix-count' => 'HTML prefix count',
- 'html-tag-batch' => 'HTML tag batch',
- 'html-matching-tag-batch' => 'HTML matching tag batch',
- 'html-matching-tag-attribute-batch' => 'HTML tag + attribute batch',
- 'html-matching-tag-attributes-batch' => 'HTML tag + attributes batch',
- 'html-link-audit-summary' => 'HTML link audit summary',
- 'html-tag-inventory-summary' => 'HTML tag inventory',
- 'html-heading-inventory-summary' => 'HTML heading inventory',
- 'html-id-inventory-summary' => 'HTML ID inventory',
- 'html-attribute-inventory-summary' => 'HTML attribute inventory',
- 'html-data-attribute-inventory-summary' => 'HTML data-* inventory',
- 'html-aria-attribute-inventory-summary' => 'HTML ARIA inventory',
- 'html-class-inventory-summary' => 'HTML class inventory',
- 'html-resource-inventory-summary' => 'HTML resource inventory',
- 'html-image-inventory-summary' => 'HTML image inventory',
- 'html-script-inventory-summary' => 'HTML script inventory',
- 'html-form-inventory-summary' => 'HTML form inventory',
- 'html-tag-prefix-batch' => 'HTML prefix batch',
- 'html-tag-prefix-count-batch' => 'HTML prefix count batch',
- 'html-tag-prefix-summary' => 'HTML prefix summary',
- 'html-tag-sanitizer' => 'HTML sanitizer',
- 'html-processor' => 'HTML fragment processor',
- 'html-token-batch' => 'HTML token batch',
- 'xml-processor' => 'XML processor scan',
- 'xml-token-summary' => 'XML token summary',
- 'xml-document-inventory-summary' => 'XML document inventory',
- 'xml-element-inventory-summary' => 'XML element inventory',
- 'xml-depth-inventory-summary' => 'XML depth inventory',
- 'xml-leaf-inventory-summary' => 'XML leaf inventory',
- 'xml-structural-inventory-summary' => 'XML structural inventory',
- 'xml-attribute-inventory-summary' => 'XML attribute inventory',
- 'xml-id-inventory-summary' => 'XML ID inventory',
- 'xml-namespace-inventory-summary' => 'XML namespace inventory',
- 'xml-text-inventory-summary' => 'XML text inventory',
- 'xml-processing-instruction-inventory-summary' => 'XML processing instruction inventory',
- 'xml-comment-inventory-summary' => 'XML comment inventory',
- 'xml-payload-inventory-summary' => 'XML payload inventory',
- 'xml-content-inventory-summary' => 'XML content inventory',
- 'xml-import-inventory-summary' => 'XML import inventory',
- 'xml-token-batch' => 'XML token batch',
- 'xml-tag-summary' => 'XML tag summary',
- 'xml-tag-batch' => 'XML tag batch',
- 'xml-matching-tag-batch' => 'XML matching tag batch',
- 'xml-matching-tag-count-batch' => 'XML matching tag count batch',
- 'xml-matching-tag-summary' => 'XML matching tag summary',
- 'xml-matching-tag-attributes-summary' => 'XML matching tag attributes summary',
- 'xml-tag-count-batch' => 'XML tag count batch',
- 'xml-prefix-summary' => 'XML prefix summary',
- 'xml-prefix-sanitizer' => 'XML prefix sanitizer',
- 'url-in-text-processor' => 'URL-in-text scan',
- );
-
- if ( isset( $labels[ $name ] ) ) {
- return $labels[ $name ];
- }
-
- return ucwords( str_replace( '-', ' ', $name ) );
-}
-
-/**
- * Infer component name from workload name.
- *
- * @param string $name Workload name.
- * @return string Component name.
- */
-function wp_toolkit_native_api_benchmark_component( $name ) {
- if ( 0 === strpos( $name, 'html-' ) ) {
- return 'HTML';
- }
- if ( 0 === strpos( $name, 'xml-' ) ) {
- return 'XML';
- }
- if ( 0 === strpos( $name, 'url-' ) ) {
- return 'URL-in-text';
- }
- return 'Native APIs';
-}
diff --git a/components/DataLiberation/Tests/DataLiberationHTMLProcessorTest.php b/components/DataLiberation/Tests/DataLiberationHTMLProcessorTest.php
index 9c8b55822..73143ebb6 100644
--- a/components/DataLiberation/Tests/DataLiberationHTMLProcessorTest.php
+++ b/components/DataLiberation/Tests/DataLiberationHTMLProcessorTest.php
@@ -4,43 +4,28 @@
use WordPress\DataLiberation\DataLiberationHTMLProcessor;
use WordPress\DataLiberation\Importer\ImportUtils;
-class NativeAwareDataLiberationHTMLProcessor extends DataLiberationHTMLProcessor {
- public function is_native_processor_active() {
- return $this->has_native_processor();
- }
-}
-
class DataLiberationHTMLProcessorTest extends TestCase {
- public function test_native_processor_remains_available_for_inherited_html_cursor_methods() {
- if ( ! class_exists( 'WP_HTML_Native_Processor', false ) ) {
- $this->markTestSkipped( 'The native HTML processor is not loaded.' );
- }
-
- $processor = NativeAwareDataLiberationHTMLProcessor::create_fragment(
+ public function test_get_inner_html_preserves_processor_position() {
+ $processor = DataLiberationHTMLProcessor::create_fragment(
'Title Body
'
);
- $this->assertTrue( $processor->is_native_processor_active() );
$this->assertTrue( $processor->next_tag( 'H1' ) );
$this->assertSame( 'Title', $processor->get_inner_html() );
- $this->assertTrue( $processor->is_native_processor_active() );
+ $this->assertSame( 'H1', $processor->get_tag() );
$this->assertTrue( $processor->next_tag( 'P' ) );
$this->assertSame( 'P', $processor->get_tag() );
}
- public function test_import_h1_removal_keeps_native_progressive_upgrade() {
- if ( ! class_exists( 'WP_HTML_Native_Processor', false ) ) {
- $this->markTestSkipped( 'The native HTML processor is not loaded.' );
- }
-
+ public function test_import_h1_removal_preserves_remaining_markup() {
$result = ImportUtils::remove_first_h1_block_from_block_markup(
- 'Native Title Body
'
+ 'Title Body
'
);
$this->assertSame(
array(
- 'h1_content' => 'Native Title',
+ 'h1_content' => 'Title',
'remaining_html' => 'Body
',
),
$result
diff --git a/components/DataLiberation/Tests/URLInTextProcessorTest.php b/components/DataLiberation/Tests/URLInTextProcessorTest.php
index 62d3e2369..a3a870c55 100644
--- a/components/DataLiberation/Tests/URLInTextProcessorTest.php
+++ b/components/DataLiberation/Tests/URLInTextProcessorTest.php
@@ -5,28 +5,6 @@
class URLInTextProcessorTest extends TestCase {
- public function test_direct_native_processor_finds_url_candidates_when_loaded() {
- $native_class = 'WordPress\\DataLiberation\\URL\\NativeURLInTextProcessor';
- if ( ! class_exists( $native_class, false ) ) {
- $this->markTestSkipped( 'Native URL-in-text processor is not loaded.' );
- }
-
- $p = new $native_class( 'Visit https://wordpress.org/plugins, then example.com/docs.' );
- $this->assertTrue( $p->next_url() );
- $this->assertSame( 'https://wordpress.org/plugins', $p->get_raw_url() );
- $this->assertSame( 6, $p->get_url_starts_at() );
- $this->assertTrue( $p->had_protocol() );
-
- $this->assertTrue( $p->next_url() );
- $this->assertSame( 'example.com/docs', $p->get_raw_url() );
- $this->assertFalse( $p->had_protocol() );
- $this->assertTrue( $p->set_raw_url( 'example.org/handbook' ) );
- $this->assertSame(
- 'Visit https://wordpress.org/plugins, then example.org/handbook.',
- $p->get_updated_text()
- );
- }
-
/**
* @dataProvider provider_test_finds_next_url_when_base_url_is_used
*/
diff --git a/components/DataLiberation/URL/class-nativeurlintextprocessorwrapper.php b/components/DataLiberation/URL/class-nativeurlintextprocessorwrapper.php
deleted file mode 100644
index a27c4f120..000000000
--- a/components/DataLiberation/URL/class-nativeurlintextprocessorwrapper.php
+++ /dev/null
@@ -1,241 +0,0 @@
-text = $text;
- $this->base_url = $base_url;
- $this->base_protocol = $base_url ? parse_url( $base_url, PHP_URL_SCHEME ) : null;
-
- if ( preg_match( '/[^\x00-\x7F]/', $text ) ) {
- $this->fallback_processor = new PHPURLInTextProcessor( $text, $base_url );
- return;
- }
-
- $native_class = 'WordPress\\DataLiberation\\URL\\NativeURLInTextProcessor';
- $this->native_processor = new $native_class( $text, $base_url );
- }
-
- public function next_url() {
- if ( null !== $this->fallback_processor ) {
- return $this->fallback_processor->next_url();
- }
-
- while ( $this->native_processor->next_url() ) {
- $this->matched_url = $this->native_processor->get_raw_url();
- $this->parsed_url = null;
- $this->url_starts_at = null;
- $this->url_length = null;
- $this->did_prepend_protocol = false;
-
- if ( false === $this->matched_url || '' === $this->matched_url || '::' === $this->matched_url ) {
- continue;
- }
-
- $url_starts_at = strpos( $this->text, $this->matched_url, $this->bytes_already_parsed );
- if ( false === $url_starts_at ) {
- continue;
- }
- $this->bytes_already_parsed = $url_starts_at + strlen( $this->matched_url );
- $this->url_starts_at = $url_starts_at;
- $this->url_length = strlen( $this->matched_url );
-
- $had_protocol = method_exists( $this->native_processor, 'had_protocol' )
- ? $this->native_processor->had_protocol()
- : WPURL::has_http_https_protocol( $this->matched_url );
-
- $cache_key = $this->base_url . "\0" . ( $had_protocol ? '1' : '0' ) . "\0" . $this->matched_url;
- $cached = self::candidate_cache()->get( $cache_key );
- if ( null !== $cached ) {
- if ( false === $cached ) {
- continue;
- }
- $this->parsed_url = $cached['parsed_url'];
- $this->did_prepend_protocol = $cached['did_prepend_protocol'];
-
- return true;
- }
-
- $parsed_url = $this->parse_and_validate_candidate( $had_protocol );
- if ( false === $parsed_url ) {
- self::candidate_cache()->set( $cache_key, false );
- continue;
- }
-
- self::candidate_cache()->set(
- $cache_key,
- array(
- 'parsed_url' => $parsed_url,
- 'did_prepend_protocol' => $this->did_prepend_protocol,
- )
- );
-
- $this->parsed_url = $parsed_url;
-
- return true;
- }
-
- return false;
- }
-
- public function get_raw_url() {
- if ( null !== $this->fallback_processor ) {
- return $this->fallback_processor->get_raw_url();
- }
-
- return $this->matched_url ?? false;
- }
-
- public function get_parsed_url() {
- if ( null !== $this->fallback_processor ) {
- return $this->fallback_processor->get_parsed_url();
- }
-
- return $this->parsed_url ?? false;
- }
-
- public function set_raw_url( $new_url ) {
- if ( null !== $this->fallback_processor ) {
- return $this->fallback_processor->set_raw_url( $new_url );
- }
-
- if ( null === $this->matched_url ) {
- return false;
- }
-
- if ( $this->did_prepend_protocol ) {
- $new_url = substr( $new_url, strpos( $new_url, '://' ) + 3 );
- }
-
- $this->matched_url = $new_url;
- $this->lexical_updates[ $this->url_starts_at ] = new WP_HTML_Text_Replacement(
- $this->url_starts_at,
- $this->url_length,
- $new_url
- );
-
- return true;
- }
-
- public function get_updated_text() {
- if ( null !== $this->fallback_processor ) {
- return $this->fallback_processor->get_updated_text();
- }
-
- $this->apply_lexical_updates();
-
- return $this->text;
- }
-
- private function parse_and_validate_candidate( $had_protocol ) {
- $preprocessed_url = $this->matched_url;
- if ( $this->base_url && $this->base_protocol && ! $had_protocol ) {
- $preprocessed_url = WPURL::ensure_protocol( $preprocessed_url, $this->base_protocol );
- $this->did_prepend_protocol = true;
- }
-
- $parsed_url = WPURL::parse( $preprocessed_url, $this->base_url );
- if ( false === $parsed_url ) {
- return false;
- }
-
- if ( $parsed_url->protocol && ! in_array( $parsed_url->protocol, array( 'http:', 'https:' ), true ) ) {
- return false;
- }
-
- if ( $parsed_url->username || $parsed_url->password ) {
- return false;
- }
-
- if ( ! $had_protocol ) {
- $last_dot_position = strrpos( $parsed_url->hostname, '.' );
- if ( false === $last_dot_position ) {
- return false;
- }
-
- $tld = substr( $parsed_url->hostname, $last_dot_position + 1 );
- if ( ! WPURL::is_known_public_domain( $tld ) ) {
- return false;
- }
- }
-
- return $parsed_url;
- }
-
- private function apply_lexical_updates() {
- if ( ! count( $this->lexical_updates ) ) {
- return;
- }
-
- ksort( $this->lexical_updates );
-
- $bytes_already_copied = 0;
- $output_buffer = '';
- foreach ( $this->lexical_updates as $diff ) {
- $shift = strlen( $diff->text ) - $diff->length;
- if ( $diff->start < $this->bytes_already_parsed ) {
- $this->bytes_already_parsed += $shift;
- }
-
- $output_buffer .= substr( $this->text, $bytes_already_copied, $diff->start - $bytes_already_copied );
- if ( $diff->start === $this->url_starts_at ) {
- $this->url_starts_at = strlen( $output_buffer );
- $this->url_length = strlen( $diff->text );
- }
- $output_buffer .= $diff->text;
- $bytes_already_copied = $diff->start + $diff->length;
- }
-
- $this->text = $output_buffer . substr( $this->text, $bytes_already_copied );
- $this->lexical_updates = array();
- }
-
- private static function candidate_cache() {
- if ( null === self::$candidate_cache ) {
- self::$candidate_cache = new URLRewriteCache( self::CANDIDATE_CACHE_MAX );
- }
-
- return self::$candidate_cache;
- }
-}
diff --git a/components/DataLiberation/URL/class-urlintextprocessor.php b/components/DataLiberation/URL/class-urlintextprocessor.php
index c5d64123a..39bfc3521 100644
--- a/components/DataLiberation/URL/class-urlintextprocessor.php
+++ b/components/DataLiberation/URL/class-urlintextprocessor.php
@@ -1,31 +1,7 @@
TitleBody
' );
- * $p->next_tag( 'H1' );
- * $inner_html = $p->get_inner_html(); // "Title".
- *
- * $p->skip_to_closer();
- * $after_h1 = substr( $html, $p->get_string_index_after_current_token() );
- *
- * Those offsets are an implementation detail of the PHP parser; native HTML
- * bookmarks are opaque and do not expose byte spans. To keep native processing
- * available for normal scanning, these helpers replay the current cursor
- * position on a PHP-backed mirror and read the offsets from that mirror.
- */
class DataLiberationHTMLProcessor extends WP_HTML_Processor {
- /**
- * Number of public tokens consumed from this processor.
- *
- * This is enough to replay the cursor onto a PHP-backed mirror for the
- * Data Liberation helpers that need PHP bookmark spans.
- *
- * @var int
- */
- private $tokens_parsed_for_php_replay = 0;
-
- /**
- * Token counts captured for public bookmarks.
- *
- * Native bookmarks are intentionally opaque. This map lets a later `seek()`
- * restore the replay counter when the bookmark was created through this
- * class, so the PHP mirror can land on the same token.
- *
- * @var array
- */
- private $php_replay_bookmarks = array();
-
- /**
- * Whether the current cursor can be replayed onto the PHP parser.
- *
- * @var bool
- */
- private $can_replay_php_cursor = true;
-
- /**
- * Whether this instance exists only as a PHP replay mirror.
- *
- * @var bool
- */
- private $is_php_replay = false;
-
- public function next_token(): bool {
- $matched = parent::next_token();
- if ( $matched ) {
- ++$this->tokens_parsed_for_php_replay;
- }
-
- return $matched;
- }
-
- public function set_bookmark( $name ): bool {
- $set = parent::set_bookmark( $name );
- if ( $set ) {
- $this->php_replay_bookmarks[ $name ] = $this->tokens_parsed_for_php_replay;
- }
-
- return $set;
- }
-
- public function release_bookmark( $name ): bool {
- $released = parent::release_bookmark( $name );
- if ( $released ) {
- unset( $this->php_replay_bookmarks[ $name ] );
- }
-
- return $released;
- }
-
- public function seek( $bookmark_name ): bool {
- $found = parent::seek( $bookmark_name );
- if ( ! $found ) {
- return false;
- }
-
- if ( isset( $this->php_replay_bookmarks[ $bookmark_name ] ) ) {
- $this->tokens_parsed_for_php_replay = $this->php_replay_bookmarks[ $bookmark_name ];
- } else {
- $this->can_replay_php_cursor = false;
- }
-
- return true;
- }
-
public function get_inner_html() {
- if ( $this->has_active_native_processor() ) {
- $php_processor = $this->create_php_replay_at_current_token();
-
- return $php_processor instanceof self ? $php_processor->get_inner_html() : false;
- }
-
if ( '#tag' !== $this->get_token_type() ) {
return false;
}
@@ -123,28 +16,24 @@ public function get_inner_html() {
return false;
}
- if ( false === $this->set_bookmark( 'tag-start' ) ) {
+ if ( false === WP_HTML_Tag_Processor::set_bookmark( 'tag-start' ) ) {
return false;
}
$this->skip_to_closer();
- if ( false === $this->set_bookmark( 'tag-end' ) ) {
- $this->release_bookmark( 'tag-start' );
+ if ( false === WP_HTML_Tag_Processor::set_bookmark( 'tag-end' ) ) {
+ WP_HTML_Tag_Processor::release_bookmark( 'tag-start' );
return false;
}
- $tag_start = $this->get_bookmark_span( 'tag-start' );
- $tag_end = $this->get_bookmark_span( 'tag-end' );
- $inner_html_start = $tag_start->start + $tag_start->length;
- $inner_html_end = $tag_end->start - $inner_html_start;
+ $inner_html_start = $this->bookmarks['tag-start']->start + $this->bookmarks['tag-start']->length;
+ $inner_html_end = $this->bookmarks['tag-end']->start - $inner_html_start;
- if ( ! $this->is_php_replay ) {
- $this->seek( 'tag-start' );
- $this->release_bookmark( 'tag-start' );
- $this->release_bookmark( 'tag-end' );
- }
+ WP_HTML_Tag_Processor::seek( 'tag-start' );
+ WP_HTML_Tag_Processor::release_bookmark( 'tag-start' );
+ WP_HTML_Tag_Processor::release_bookmark( 'tag-end' );
return substr(
$this->html,
@@ -169,97 +58,11 @@ public function skip_to_closer() {
}
public function get_string_index_after_current_token() {
- if ( $this->has_active_native_processor() ) {
- $php_processor = $this->create_php_replay_at_current_token();
-
- return $php_processor instanceof self ? $php_processor->get_string_index_after_current_token() : false;
- }
-
$name = 'current_token';
$this->set_bookmark( $name );
- $bookmark = $this->get_bookmark_span( '_' . $name );
+ $bookmark = $this->bookmarks[ '_' . $name ];
$this->release_bookmark( $name );
return $bookmark->start + $bookmark->length;
}
-
- /**
- * Returns a PHP bookmark span from this object or its PHP delegate.
- *
- * @param string $name Bookmark name.
- * @return WP_HTML_Span Bookmark span.
- */
- private function get_bookmark_span( $name ) {
- if ( isset( $this->bookmarks[ $name ] ) ) {
- return $this->bookmarks[ $name ];
- }
- if ( isset( $this->bookmarks[ '_' . $name ] ) ) {
- return $this->bookmarks[ '_' . $name ];
- }
-
- if ( property_exists( $this, 'php_processor' ) ) {
- $property = new \ReflectionProperty( 'WP_HTML_PHP_Tag_Processor', 'bookmarks' );
- $property->setAccessible( true );
- $bookmarks = $property->getValue( $this->php_processor );
- if ( isset( $bookmarks[ $name ] ) ) {
- return $bookmarks[ $name ];
- }
- if ( isset( $bookmarks[ '_' . $name ] ) ) {
- return $bookmarks[ '_' . $name ];
- }
- }
-
- return null;
- }
-
- /**
- * Checks whether this processor has a native delegate.
- *
- * The pure PHP HTML Processor loaded on older runtimes does not define the
- * native-wrapper helper method, so callers must guard the method before
- * invoking it.
- *
- * @return bool Whether a native processor is active.
- */
- private function has_active_native_processor() {
- return method_exists( $this, 'has_native_processor' ) && $this->has_native_processor();
- }
-
- /**
- * Creates a PHP-backed processor at the same public cursor token.
- *
- * Native cursor operations are faster for scanning, but native bookmarks do
- * not expose byte offsets. For offset-only helpers this method replays the
- * already-consumed public token count on a mirror with native delegation
- * disabled. Example:
- *
- * $native = DataLiberationHTMLProcessor::create_fragment( 'Title Body
' );
- * $native->next_tag( 'H1' );
- *
- * // The mirror runs the PHP parser through the same first token, so its
- * // bookmark spans point to the original `` source bytes.
- * $php = $native->create_php_replay_at_current_token();
- *
- * @return self|null PHP-backed processor at the current token, or null.
- */
- private function create_php_replay_at_current_token() {
- if ( ! $this->can_replay_php_cursor ) {
- return null;
- }
-
- $processor = static::create_fragment( $this->html );
- if ( ! $processor instanceof self ) {
- return null;
- }
-
- $processor->set_native_processor( null );
- $processor->is_php_replay = true;
- for ( $i = 0; $i < $this->tokens_parsed_for_php_replay; ++$i ) {
- if ( ! $processor->next_token() ) {
- return null;
- }
- }
-
- return $processor;
- }
}
diff --git a/components/HTML/PHP/class-wp-html-php-processor.php b/components/HTML/PHP/class-wp-html-php-processor.php
index 41c2d7ae0..5645ac928 100644
--- a/components/HTML/PHP/class-wp-html-php-processor.php
+++ b/components/HTML/PHP/class-wp-html-php-processor.php
@@ -140,7 +140,7 @@
* @see WP_HTML_Tag_Processor
* @see https://html.spec.whatwg.org/
*/
-class WP_HTML_PHP_Processor extends WP_HTML_PHP_Tag_Processor {
+class WP_HTML_PHP_Processor extends WP_HTML_Tag_Processor {
/**
* The maximum number of bookmarks allowed to exist at any given time.
*
diff --git a/components/HTML/README.md b/components/HTML/README.md
index d2d30b1e4..e5c4400cd 100644
--- a/components/HTML/README.md
+++ b/components/HTML/README.md
@@ -14,16 +14,7 @@ see_also:
- dataliberation | DataLiberation | Rewrite URLs and media references during import/export pipelines.
---
-A pure-PHP HTML5 parser and tag rewriter mirroring WordPress core's HTML API. Treat HTML the way browsers do — without libxml2, DOMDocument, or regex hacks — and rewrite attributes in a single linear pass.
-
-When the native API extension is loaded, tag and fragment processors can use
-native delegates by default while preserving PHP fallback behavior. Define
-WP_NATIVE_APIS_DISABLE_DEFAULTS before loading the component to
-force the pure PHP fallback. Full-document parsing through
-WP_HTML_Processor::create_full_parser() remains PHP-backed for
-now. Fragment processors, including covered table, list, description-list,
-select/option/optgroup, omitted-paragraph, and ruby tree-builder cases, can use
-native delegates when the extension is loaded.
+A pure-PHP HTML parser and tag rewriter mirroring WordPress core's HTML API. Handle browser-style HTML fragments for supported markup — without libxml2, DOMDocument, or regex hacks — and rewrite attributes in a single linear pass.
## Why this exists
@@ -33,6 +24,8 @@ native delegates when the extension is loaded.
The component gives you two processors. WP_HTML_Tag_Processor is a forward-only cursor over tags and tokens — useful for attribute rewriting at scale. WP_HTML_Processor layers HTML5 tree construction on top so you can query by ancestry (breadcrumbs), serialize the parsed document, and trust that <p>one<p>two parses as two paragraphs the way a browser sees it.
+Scope: WP_HTML_Processor intentionally supports WordPress core's current subset of HTML5. It aborts on markup it cannot safely model, including table-internal content, foreign content such as SVG/MathML, and content outside the supported body parsing modes. Use get_unsupported_exception() when you need to explain why processing stopped.
+
Footgun: Mutations are buffered. Nothing changes in the source string until you call get_updated_html(). If you read get_attribute() after a set_attribute() on the same tag, you see the new value — but downstream tooling reading the original string sees stale HTML until you serialize.
## Add loading="lazy" to every image
diff --git a/components/HTML/Tests/NativeHTMLConformanceTest.php b/components/HTML/Tests/NativeHTMLConformanceTest.php
deleted file mode 100644
index ec0a7aa38..000000000
--- a/components/HTML/Tests/NativeHTMLConformanceTest.php
+++ /dev/null
@@ -1,4007 +0,0 @@
-markTestSkipped( 'Native HTML classes are not registered; load the native API extension to run this case.' );
- }
-
- $tag_processor = new WP_HTML_Tag_Processor( 'Text
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_tag() );
- $this->assertSame( 'P', $tag_processor->get_tag() );
- $this->assertSame( '1', $tag_processor->get_attribute( 'data-id' ) );
- $this->assertTrue( $tag_processor->remove_attribute( 'data-id' ) );
- $this->assertNull( $tag_processor->get_attribute( 'data-id' ) );
- $this->assertSame( 'Text
', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertTrue( $tag_processor->next_tag( array( 'tag_closers' => 'visit' ) ) );
- $this->assertSame( 'P', $tag_processor->get_tag() );
- $this->assertFalse( $tag_processor->is_tag_closer() );
- $this->assertTrue( $tag_processor->next_tag( array( 'tag_closers' => 'visit' ) ) );
- $this->assertSame( 'BR', $tag_processor->get_tag() );
- $this->assertFalse( $tag_processor->is_tag_closer() );
- $this->assertTrue( $tag_processor->next_tag( array( 'tag_closers' => 'visit' ) ) );
- $this->assertSame( 'P', $tag_processor->get_tag() );
- $this->assertTrue( $tag_processor->is_tag_closer() );
- $this->assertNull( $tag_processor->get_attribute_names_with_prefix( 'data-' ) );
- $this->assertFalse( $tag_processor->has_class( 'anything' ) );
-
- $tag_processor = new WP_HTML_Tag_Processor( 'One
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_token() );
- $this->assertTrue( $tag_processor->next_token() );
- $this->assertSame( '#text', $tag_processor->get_token_type() );
- $this->assertTrue( $tag_processor->set_modifiable_text( 'Two & Three' ) );
- $this->assertSame( 'Two & Three
', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( " \tMore" );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_token() );
- $this->assertTrue( $tag_processor->subdivide_text_appropriately() );
- $this->assertSame( " \t", $tag_processor->get_modifiable_text() );
- $this->assertTrue( $tag_processor->next_token() );
- $this->assertSame( 'More', $tag_processor->get_modifiable_text() );
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertSame(
- array(
- array(
- 'tag_name' => 'P',
- 'is_tag_closer' => false,
- ),
- ),
- $tag_processor->next_tag_summary_batch( 1 )
- );
- $this->assertSame( array( 'a', 'b' ), iterator_to_array( $tag_processor->class_list() ) );
- $this->assertSame( 'intro', $tag_processor->get_attribute( 'data-kind' ) );
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertSame(
- array(
- array(
- 'tag_name' => 'P',
- 'is_tag_closer' => false,
- 'attribute_count' => 1,
- ),
- ),
- $tag_processor->next_tag_prefix_summary_batch( 'data-', 1 )
- );
- $this->assertSame( array( 'a', 'b' ), iterator_to_array( $tag_processor->class_list() ) );
- $this->assertSame( 'intro', $tag_processor->get_attribute( 'data-kind' ) );
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertSame( "1\x1f1", $tag_processor->next_tag_prefix_count_compact_batch( 'data-', 1 ) );
- $this->assertSame( array( 'a', 'b' ), iterator_to_array( $tag_processor->class_list() ) );
- $this->assertSame( 'intro', $tag_processor->get_attribute( 'data-kind' ) );
-
- $html_processor = WP_HTML_Processor::create_fragment( 'Text
' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $html_processor ) ) );
- $this->assertTrue( $html_processor->next_token() );
- $this->assertSame( 'P', $html_processor->get_token_name() );
- $this->assertSame( array( 'HTML', 'BODY', 'P' ), $html_processor->get_breadcrumbs() );
-
- $list_processor = WP_HTML_Processor::create_fragment( '' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $list_processor ) ) );
- $this->assertTrue( $list_processor->next_token() );
- $this->assertSame( 'UL', $list_processor->get_token_name() );
- $this->assertTrue( $list_processor->next_token() );
- $this->assertSame( 'LI', $list_processor->get_token_name() );
- $this->assertFalse( $list_processor->is_tag_closer() );
- $this->assertTrue( $list_processor->next_token() );
- $this->assertSame( 'one', $list_processor->get_modifiable_text() );
- $this->assertTrue( $list_processor->next_token() );
- $this->assertSame( 'LI', $list_processor->get_token_name() );
- $this->assertTrue( $list_processor->is_tag_closer() );
-
- $description_list_processor = WP_HTML_Processor::create_fragment( 'A B ' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $description_list_processor ) ) );
- $this->assertTrue( $description_list_processor->next_token() );
- $this->assertSame( 'DL', $description_list_processor->get_token_name() );
- $this->assertTrue( $description_list_processor->next_token() );
- $this->assertSame( 'DT', $description_list_processor->get_token_name() );
- $this->assertFalse( $description_list_processor->is_tag_closer() );
- $this->assertTrue( $description_list_processor->next_token() );
- $this->assertSame( 'A', $description_list_processor->get_modifiable_text() );
- $this->assertTrue( $description_list_processor->next_token() );
- $this->assertSame( 'DT', $description_list_processor->get_token_name() );
- $this->assertTrue( $description_list_processor->is_tag_closer() );
-
- $select_processor = WP_HTML_Processor::create_fragment( 'A B ' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $select_processor ) ) );
- $this->assertTrue( $select_processor->next_token() );
- $this->assertSame( 'SELECT', $select_processor->get_token_name() );
- $this->assertTrue( $select_processor->next_token() );
- $this->assertSame( 'OPTION', $select_processor->get_token_name() );
- $this->assertFalse( $select_processor->is_tag_closer() );
- $this->assertTrue( $select_processor->next_token() );
- $this->assertSame( 'A', $select_processor->get_modifiable_text() );
- $this->assertTrue( $select_processor->next_token() );
- $this->assertSame( 'OPTION', $select_processor->get_token_name() );
- $this->assertTrue( $select_processor->is_tag_closer() );
-
- $optgroup_processor = WP_HTML_Processor::create_fragment( 'A B ' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $optgroup_processor ) ) );
- $this->assertTrue( $optgroup_processor->next_token() );
- $this->assertSame( 'SELECT', $optgroup_processor->get_token_name() );
- $this->assertTrue( $optgroup_processor->next_token() );
- $this->assertSame( 'OPTGROUP', $optgroup_processor->get_token_name() );
- $this->assertFalse( $optgroup_processor->is_tag_closer() );
- $this->assertTrue( $optgroup_processor->next_tag( array( 'tag_closers' => 'visit' ) ) );
- $this->assertSame( 'OPTION', $optgroup_processor->get_token_name() );
- $this->assertFalse( $optgroup_processor->is_tag_closer() );
- $this->assertTrue( $optgroup_processor->next_tag( array( 'tag_closers' => 'visit' ) ) );
- $this->assertSame( 'OPTION', $optgroup_processor->get_token_name() );
- $this->assertTrue( $optgroup_processor->is_tag_closer() );
- $this->assertTrue( $optgroup_processor->next_token() );
- $this->assertSame( 'OPTGROUP', $optgroup_processor->get_token_name() );
- $this->assertTrue( $optgroup_processor->is_tag_closer() );
-
- $ruby_processor = WP_HTML_Processor::create_fragment( 'ab(c ' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $ruby_processor ) ) );
- $this->assertTrue( $ruby_processor->next_token() );
- $this->assertSame( 'RUBY', $ruby_processor->get_token_name() );
- $this->assertTrue( $ruby_processor->next_token() );
- $this->assertSame( 'a', $ruby_processor->get_modifiable_text() );
- $this->assertTrue( $ruby_processor->next_token() );
- $this->assertSame( 'RT', $ruby_processor->get_token_name() );
- $this->assertFalse( $ruby_processor->is_tag_closer() );
- $this->assertTrue( $ruby_processor->next_token() );
- $this->assertSame( 'b', $ruby_processor->get_modifiable_text() );
- $this->assertTrue( $ruby_processor->next_token() );
- $this->assertSame( 'RT', $ruby_processor->get_token_name() );
- $this->assertTrue( $ruby_processor->is_tag_closer() );
-
- $paragraph_processor = WP_HTML_Processor::create_fragment( 'one
two
' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $paragraph_processor ) ) );
- $this->assertTrue( $paragraph_processor->next_token() );
- $this->assertSame( 'P', $paragraph_processor->get_token_name() );
- $this->assertFalse( $paragraph_processor->is_tag_closer() );
- $this->assertTrue( $paragraph_processor->next_token() );
- $this->assertSame( 'one', $paragraph_processor->get_modifiable_text() );
- $this->assertTrue( $paragraph_processor->next_token() );
- $this->assertSame( 'P', $paragraph_processor->get_token_name() );
- $this->assertTrue( $paragraph_processor->is_tag_closer() );
- $this->assertTrue( $paragraph_processor->next_token() );
- $this->assertSame( 'DIV', $paragraph_processor->get_token_name() );
- $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $paragraph_processor->get_breadcrumbs() );
-
- $table_processor = WP_HTML_Processor::create_fragment( '' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $table_processor ) ) );
- $this->assertTrue( $table_processor->next_token() );
- $this->assertSame( 'TABLE', $table_processor->get_token_name() );
- $this->assertTrue( $table_processor->next_token() );
- $this->assertSame( 'TBODY', $table_processor->get_token_name() );
- $this->assertFalse( $table_processor->is_tag_closer() );
-
- $full_processor = WP_HTML_Processor::create_full_parser( 'Text ' );
- $this->assertNull( $this->get_native_delegate( $full_processor ) );
- $this->assertTrue( $full_processor->next_token() );
- $this->assertSame( 'HTML', $full_processor->get_token_name() );
- }
-
- /**
- * Verifies public native-backed tag queries match PHP fallback match-offset parsing.
- */
- public function test_public_html_native_defaults_ignore_non_integer_match_offsets() {
- if ( ! class_exists( 'WP_HTML_Native_Tag_Processor', false ) || ! method_exists( 'WP_HTML_Native_Tag_Processor', 'supports_public_api' ) ) {
- $this->markTestSkipped( 'Native HTML classes are not registered; load the native API extension to run this case.' );
- }
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_tag( array( 'tag_name' => 'p', 'match_offset' => '2' ) ) );
- $this->assertSame( 'one', $tag_processor->get_attribute( 'id' ) );
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_tag( array( 'tag_name' => 'p', 'match_offset' => 2.0 ) ) );
- $this->assertSame( 'one', $tag_processor->get_attribute( 'id' ) );
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_tag( array( 'tag_name' => 'p', 'match_offset' => 2 ) ) );
- $this->assertSame( 'two', $tag_processor->get_attribute( 'id' ) );
- }
-
- /**
- * Verifies public native-backed unrestricted closer fast paths do not swallow
- * tag-name queries that also visit closers.
- */
- public function test_public_html_native_defaults_honor_tag_name_when_visiting_closers() {
- if ( ! class_exists( 'WP_HTML_Native_Tag_Processor', false ) || ! method_exists( 'WP_HTML_Native_Tag_Processor', 'supports_public_api' ) ) {
- $this->markTestSkipped( 'Native HTML classes are not registered; load the native API extension to run this case.' );
- }
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue(
- $tag_processor->next_tag(
- array(
- 'tag_name' => 'p',
- 'tag_closers' => 'visit',
- )
- )
- );
- $this->assertSame( 'P', $tag_processor->get_tag() );
- $this->assertFalse( $tag_processor->is_tag_closer() );
- $this->assertSame( 'target', $tag_processor->get_attribute( 'id' ) );
- }
-
- /**
- * Verifies public native-backed tag processors handle invalid query types like PHP fallback.
- */
- public function test_public_html_native_defaults_treat_invalid_query_types_as_unrestricted() {
- if ( ! class_exists( 'WP_HTML_Native_Tag_Processor', false ) || ! method_exists( 'WP_HTML_Native_Tag_Processor', 'supports_public_api' ) ) {
- $this->markTestSkipped( 'Native HTML classes are not registered; load the native API extension to run this case.' );
- }
-
- $tag_processor = new WP_HTML_Tag_Processor( '
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_tag( 42 ) );
- $this->assertSame( 'P', $tag_processor->get_tag() );
- $this->assertSame( 'one', $tag_processor->get_attribute( 'id' ) );
- }
-
- /**
- * Verifies HTML native defaults can be disabled by constant.
- */
- public function test_public_html_classes_can_disable_native_defaults_by_constant() {
- if ( ! class_exists( 'WP_HTML_Native_Tag_Processor', false ) || ! method_exists( 'WP_HTML_Native_Tag_Processor', 'supports_public_api' ) || ! class_exists( 'WP_HTML_Native_Processor', false ) || ! method_exists( 'WP_HTML_Native_Processor', 'supports_public_api' ) ) {
- $this->markTestSkipped( 'Native HTML classes are not registered; load the native API extension to run this case.' );
- }
-
- $this->assertSame(
- array(
- 'tag:php',
- 'tag-id:1',
- 'tree:php',
- 'tree-token:P',
- 'full:php',
- 'full-token:HTML',
- ),
- $this->run_html_native_defaults_constant_probe( "define( 'WP_NATIVE_APIS_DISABLE_DEFAULTS', true );" )
- );
- }
-
- /**
- * Verifies public native-backed HTML processors apply attribute and class updates.
- */
- public function test_public_html_mutations_with_native_defaults() {
- if ( ! class_exists( 'WP_HTML_Native_Tag_Processor', false ) || ! method_exists( 'WP_HTML_Native_Tag_Processor', 'supports_public_api' ) || ! class_exists( 'WP_HTML_Native_Processor', false ) || ! method_exists( 'WP_HTML_Native_Processor', 'supports_public_api' ) ) {
- $this->markTestSkipped( 'Native HTML classes are not registered; load the native API extension to run this case.' );
- }
-
- $tag_processor = new WP_HTML_Tag_Processor( 'Text
' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertTrue( $tag_processor->set_attribute( 'data-id', '7' ) );
- $this->assertSame( '7', $tag_processor->get_attribute( 'data-id' ) );
- $this->assertTrue( $tag_processor->add_class( 'b' ) );
- $this->assertSame( 'a b', $tag_processor->get_attribute( 'class' ) );
- $this->assertSame( 'Text
', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( ' ' );
- $this->assertTrue( $tag_processor->next_tag( 'img' ) );
- $this->assertTrue( $tag_processor->set_attribute( 'src', 'https://w.org/logo.png' ) );
- $this->assertTrue( $tag_processor->set_attribute( 'alt', 'An image' ) );
- $this->assertSame( 'https://w.org/logo.png', $tag_processor->get_attribute( 'src' ) );
- $this->assertSame( 'An image', $tag_processor->get_attribute( 'alt' ) );
- $this->assertSame( ' ', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( 'Text
' );
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertTrue( $tag_processor->get_attribute( 'hidden' ) );
- $this->assertTrue( $tag_processor->set_attribute( 'hidden', true ) );
- $this->assertTrue( $tag_processor->get_attribute( 'hidden' ) );
- $this->assertSame( 'Text
', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( 'Text
' );
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertTrue( $tag_processor->remove_class( 'a' ) );
- $this->assertSame( 'b', $tag_processor->get_attribute( 'class' ) );
- $this->assertSame( 'Text
', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( 'Text
' );
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertTrue( $tag_processor->set_attribute( 'data-id', '7' ) );
- $this->assertFalse( $tag_processor->remove_attribute( 'data-id' ) );
- $this->assertNull( $tag_processor->get_attribute( 'data-id' ) );
- $this->assertSame( 'Text
', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( 'Text
' );
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertTrue( $tag_processor->add_class( 'a' ) );
- $this->assertTrue( $tag_processor->remove_class( 'a' ) );
- $this->assertNull( $tag_processor->get_attribute( 'class' ) );
- $this->assertSame( 'Text
', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( 'Text
' );
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertTrue( $tag_processor->remove_class( 'a' ) );
- $this->assertTrue( $tag_processor->add_class( 'a' ) );
- $this->assertSame( 'a b', $tag_processor->get_attribute( 'class' ) );
- $this->assertSame( 'Text
', $tag_processor->get_updated_html() );
-
- $tag_processor = new WP_HTML_Tag_Processor( 'Text
' );
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertTrue( $tag_processor->remove_class( 'a' ) );
- $this->assertTrue( $tag_processor->add_class( 'b' ) );
- $this->assertSame( 'b', $tag_processor->get_attribute( 'class' ) );
- $this->assertSame( 'Text
', $tag_processor->get_updated_html() );
-
- $html_processor = WP_HTML_Processor::create_fragment( '' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $html_processor ) ) );
- $this->assertTrue( $html_processor->next_tag( 'P' ) );
- $this->assertTrue( $html_processor->set_attribute( 'data-id', '7' ) );
- $this->assertSame( '7', $html_processor->get_attribute( 'data-id' ) );
- $this->assertTrue( $html_processor->add_class( 'b' ) );
- $this->assertSame( 'a b', $html_processor->get_attribute( 'class' ) );
- $this->assertSame( '', $html_processor->get_updated_html() );
-
- $html_processor = WP_HTML_Processor::create_fragment( '' );
- $this->assertTrue( $html_processor->next_tag( 'P' ) );
- $this->assertTrue( $html_processor->get_attribute( 'hidden' ) );
- $this->assertTrue( $html_processor->set_attribute( 'hidden', true ) );
- $this->assertTrue( $html_processor->get_attribute( 'hidden' ) );
- $this->assertSame( '', $html_processor->get_updated_html() );
-
- $html_processor = WP_HTML_Processor::create_fragment( '' );
- $this->assertTrue( $html_processor->next_tag( 'P' ) );
- $this->assertTrue( $html_processor->set_attribute( 'data-id', '7' ) );
- $this->assertFalse( $html_processor->remove_attribute( 'data-id' ) );
- $this->assertNull( $html_processor->get_attribute( 'data-id' ) );
- $this->assertSame( '', $html_processor->get_updated_html() );
- }
-
- /**
- * Verifies public native-backed tag processors preserve bookmark lifecycle behavior.
- */
- public function test_public_html_tag_processor_bookmarks_with_native_defaults() {
- if ( ! class_exists( 'WP_HTML_Native_Tag_Processor', false ) || ! method_exists( 'WP_HTML_Native_Tag_Processor', 'supports_public_api' ) ) {
- $this->markTestSkipped( 'Native HTML classes are not registered; load the native API extension to run this case.' );
- }
-
- $tag_processor = new WP_HTML_Tag_Processor( '' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
-
- $this->assertTrue( $tag_processor->next_tag( 'span' ) );
- $this->assertSame( 'SPAN', $tag_processor->get_tag() );
- $this->assertSame( 'two', $tag_processor->get_attribute( 'id' ) );
- $this->assertTrue( $tag_processor->set_bookmark( 'saved-span' ) );
- $this->assertTrue( $tag_processor->has_bookmark( 'saved-span' ) );
-
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertSame( 'P', $tag_processor->get_tag() );
- $this->assertSame( 'three', $tag_processor->get_attribute( 'id' ) );
-
- $this->assertTrue( $tag_processor->seek( 'saved-span' ) );
- $this->assertSame( 'SPAN', $tag_processor->get_tag() );
- $this->assertSame( 'two', $tag_processor->get_attribute( 'id' ) );
- $this->assertTrue( $tag_processor->remove_attribute( 'id' ) );
- $this->assertSame( '', $tag_processor->get_updated_html() );
- $this->assertTrue( $tag_processor->release_bookmark( 'saved-span' ) );
- $this->assertFalse( $tag_processor->has_bookmark( 'saved-span' ) );
-
- $tag_processor = new WP_HTML_Tag_Processor( '' );
- $this->assertSame( 'WP_HTML_Native_Tag_Processor', get_class( $this->get_native_delegate( $tag_processor ) ) );
- $this->assertTrue( $tag_processor->next_tag( 'span' ) );
- $this->assertTrue( $tag_processor->set_bookmark( 'saved-class' ) );
- $this->assertTrue( $tag_processor->next_tag( 'p' ) );
- $this->assertTrue( $tag_processor->seek( 'saved-class' ) );
- $this->assertTrue( $tag_processor->remove_class( 'b' ) );
- $this->assertTrue( $tag_processor->add_class( 'c' ) );
- $this->assertSame( 'c', $tag_processor->get_attribute( 'class' ) );
- $this->assertSame( '', $tag_processor->get_updated_html() );
- }
-
- /**
- * Verifies public native-backed HTML processors preserve bookmark lifecycle behavior.
- */
- public function test_public_html_processor_bookmarks_with_native_defaults() {
- if ( ! class_exists( 'WP_HTML_Native_Processor', false ) || ! method_exists( 'WP_HTML_Native_Processor', 'supports_public_api' ) ) {
- $this->markTestSkipped( 'WP_HTML_Native_Processor is not registered; load the native API extension to run this case.' );
- }
-
- $processor = WP_HTML_Processor::create_fragment( '' );
- $this->assertSame( 'WP_HTML_Native_Processor', get_class( $this->get_native_delegate( $processor ) ) );
-
- $this->assertTrue( $processor->next_tag( 'P' ) );
- $this->assertSame( 'P', $processor->get_tag() );
- $this->assertSame( 'one', $processor->get_attribute( 'id' ) );
- $this->assertTrue( $processor->set_bookmark( 'saved-paragraph' ) );
- $this->assertTrue( $processor->has_bookmark( 'saved-paragraph' ) );
-
- $this->assertTrue( $processor->next_tag( 'P' ) );
- $this->assertSame( 'P', $processor->get_tag() );
- $this->assertSame( 'two', $processor->get_attribute( 'id' ) );
-
- $this->assertTrue( $processor->seek( 'saved-paragraph' ) );
- $this->assertSame( 'P', $processor->get_tag() );
- $this->assertSame( 'one', $processor->get_attribute( 'id' ) );
- $this->assertSame( array( 'HTML', 'BODY', 'SECTION', 'P' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->release_bookmark( 'saved-paragraph' ) );
- $this->assertFalse( $processor->has_bookmark( 'saved-paragraph' ) );
- }
-
- /**
- * Verifies public HTML normalization keeps PHP serializer semantics with native defaults.
- */
- public function test_public_html_normalize_uses_php_serializer_with_native_defaults() {
- if ( ! class_exists( 'WP_HTML_Native_Processor', false ) || ! method_exists( 'WP_HTML_Native_Processor', 'supports_public_api' ) ) {
- $this->markTestSkipped( 'WP_HTML_Native_Processor is not registered; load the native API extension to run this case.' );
- }
-
- $html = 'One Link '
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the first HTML tag.' );
- $this->assertSame( 'MAIN', $processor->get_tag() );
- $this->assertSame( '7', $processor->get_attribute( 'data-z' ) );
- $this->assertSame( 'post', $processor->get_attribute( 'data-a' ) );
- $this->assertSame(
- array( 'data-z', 'data-a' ),
- $processor->get_attribute_names_with_prefix( 'data-' )
- );
- $this->skip_if_html_method_is_unavailable( $processor, 'count_attribute_names_with_prefix' );
- $this->assertSame( 2, $processor->count_attribute_names_with_prefix( 'data-' ) );
- $this->assertSame( 0, $processor->count_attribute_names_with_prefix( 'aria-' ) );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the nested anchor tag.' );
- $this->assertSame( 'A', $processor->get_tag() );
- $this->assertSame( '/x?one=1&two=2', $processor->get_attribute( 'href' ) );
- $this->assertSame( 'A/B/C', $processor->get_attribute( 'title' ) );
- $this->assertSame( "A\u{00a0}B\u{00a9}\u{00ae}\u{2026}\u{2014}\u{2209}", $processor->get_attribute( 'data-label' ) );
- $this->assertFalse( $processor->next_tag(), 'next_tag() should skip closing tags by default.' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Text
'
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the paragraph tag with unusual attributes.' );
- $this->assertSame( array( '.x', '@x', 'data-x' ), $processor->get_attribute_names_with_prefix( '' ) );
- $this->assertSame( 'dot', $processor->get_attribute( '.x' ) );
- $this->assertSame( 'at', $processor->get_attribute( '@x' ) );
- $this->assertNull( $processor->get_attribute( 'x' ) );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Text
'
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the paragraph tag with equals-start attributes.' );
- $this->assertSame( array( '=b', 'a', '=c', 'data-x' ), $processor->get_attribute_names_with_prefix( '' ) );
- $this->assertTrue( $processor->get_attribute( '=b' ) );
- $this->assertTrue( $processor->get_attribute( '=c' ) );
- $this->assertNull( $processor->get_attribute( 'b' ) );
- $this->assertSame( array( '=b', '=c' ), $processor->get_attribute_names_with_prefix( '=' ) );
- $this->assertSame( 2, $processor->count_attribute_names_with_prefix( '=' ) );
-
- $processor = $this->create_tag_processor(
- $implementation,
- '
'
- );
-
- $this->assertTrue( $this->next_tag_visiting_closers( $processor ), 'Expected the opening tag.' );
- $this->assertSame( array( 'data-id' ), $processor->get_attribute_names_with_prefix( 'data-' ) );
- $this->assertSame( 1, $processor->count_attribute_names_with_prefix( 'data-' ) );
- $this->assertTrue( $this->next_tag_visiting_closers( $processor ), 'Expected the closing tag.' );
- $this->assertTrue( $processor->is_tag_closer() );
- $this->assertNull( $processor->get_attribute_names_with_prefix( 'data-' ) );
- $this->assertNull( $processor->count_attribute_names_with_prefix( 'data-' ) );
- }
-
- /**
- * Verifies attribute removal works for the shared sanitization workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_removes_attributes( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the main tag.' );
- $this->assertSame( array( 'data-id' ), $processor->get_attribute_names_with_prefix( 'data-' ) );
- $this->assertTrue( $processor->remove_attribute( 'data-id' ) );
- $this->assertNull( $processor->get_attribute( 'data-id' ) );
- $this->assertSame(
- 'Link ',
- $processor->get_updated_html()
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the anchor tag after removing from the main tag.' );
- $this->skip_if_html_method_is_unavailable( $processor, 'remove_attributes_with_prefix' );
- $this->assertSame( 2, $processor->remove_attributes_with_prefix( 'data-' ) );
- $this->assertNull( $processor->get_attribute( 'data-kind' ) );
- $this->assertSame( 0, $processor->remove_attributes_with_prefix( 'data-' ) );
- $this->assertSame(
- 'Link ',
- $processor->get_updated_html()
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Text
'
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the paragraph tag with boolean attributes.' );
- $this->assertTrue( $processor->remove_attribute( 'disabled' ) );
- $this->assertSame( 'Text
', $processor->get_updated_html() );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Text
'
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the paragraph tag with boolean attributes.' );
- $this->assertSame( 3, $processor->remove_attributes_with_prefix( 'd' ) );
- $this->assertSame( 'Text
', $processor->get_updated_html() );
- }
-
- /**
- * Verifies document-level prefix removals match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_removes_prefixed_attributes_from_document( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 5,
- 'removed_count' => 4,
- 'html' => 'Link ',
- ),
- $this->remove_attributes_with_prefix_from_document( $processor, 'data-', true )
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- ' '
- );
-
- $this->assertTrue( $this->next_tag_visiting_closers( $processor ), 'Expected the opening main tag.' );
- $this->assertSame(
- array(
- 'tag_count' => 1,
- 'removed_count' => 1,
- 'html' => ' ',
- ),
- $this->remove_attributes_with_prefix_from_document( $processor, 'data-', false )
- );
- }
-
- /**
- * Verifies document-level prefix summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_prefixed_attributes( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 5,
- 'attribute_count' => 3,
- ),
- $this->summarize_attribute_names_with_prefix( $processor, 'data-', true )
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 3,
- 'attribute_count' => 3,
- ),
- $this->summarize_attribute_names_with_prefix( $processor, 'data-', false )
- );
- }
-
- /**
- * Verifies document-level tag inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_tag_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 5,
- 'open_tag_count' => 3,
- 'closing_tag_count' => 2,
- 'attribute_count' => 4,
- 'unique_tag_name_count' => 3,
- ),
- $this->summarize_tag_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 3,
- 'open_tag_count' => 3,
- 'closing_tag_count' => 0,
- 'attribute_count' => 4,
- 'unique_tag_name_count' => 3,
- ),
- $this->summarize_tag_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies heading inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_heading_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = 'A ';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 10,
- 'heading_count' => 3,
- 'h1_count' => 1,
- 'h2_count' => 1,
- 'h3_count' => 1,
- 'h4_count' => 0,
- 'h5_count' => 0,
- 'h6_count' => 0,
- ),
- $this->summarize_heading_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 5,
- 'heading_count' => 3,
- 'h1_count' => 1,
- 'h2_count' => 1,
- 'h3_count' => 1,
- 'h4_count' => 0,
- 'h5_count' => 0,
- 'h6_count' => 0,
- ),
- $this->summarize_heading_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies ID inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_id_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = 'One
';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 8,
- 'id_tag_count' => 4,
- 'unique_id_count' => 2,
- 'duplicate_id_count' => 1,
- 'id_value_bytes' => 14,
- ),
- $this->summarize_id_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 4,
- 'id_tag_count' => 4,
- 'unique_id_count' => 2,
- 'duplicate_id_count' => 1,
- 'id_value_bytes' => 14,
- ),
- $this->summarize_id_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies attribute inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_attribute_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = 'Text
';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 4,
- 'attribute_count' => 4,
- 'unique_attribute_name_count' => 4,
- 'attribute_value_bytes' => 7,
- ),
- $this->summarize_attribute_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 2,
- 'attribute_count' => 4,
- 'unique_attribute_name_count' => 4,
- 'attribute_value_bytes' => 7,
- ),
- $this->summarize_attribute_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies data-attribute inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_data_attribute_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = '
';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 4,
- 'data_attribute_tag_count' => 2,
- 'data_attribute_count' => 5,
- 'unique_data_attribute_name_count' => 4,
- 'data_attribute_value_bytes' => 14,
- ),
- $this->summarize_data_attribute_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 2,
- 'data_attribute_tag_count' => 2,
- 'data_attribute_count' => 5,
- 'unique_data_attribute_name_count' => 4,
- 'data_attribute_value_bytes' => 14,
- ),
- $this->summarize_data_attribute_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies ARIA attribute inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_aria_attribute_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = '
';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 4,
- 'aria_attribute_tag_count' => 2,
- 'aria_attribute_count' => 4,
- 'unique_aria_attribute_name_count' => 3,
- 'aria_attribute_value_bytes' => 15,
- ),
- $this->summarize_aria_attribute_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 2,
- 'aria_attribute_tag_count' => 2,
- 'aria_attribute_count' => 4,
- 'unique_aria_attribute_name_count' => 3,
- 'aria_attribute_value_bytes' => 15,
- ),
- $this->summarize_aria_attribute_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies class inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_class_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = 'Text
Bool
';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 8,
- 'class_attribute_count' => 4,
- 'class_name_count' => 6,
- 'unique_class_name_count' => 5,
- 'class_value_bytes' => 40,
- ),
- $this->summarize_class_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 4,
- 'class_attribute_count' => 4,
- 'class_name_count' => 6,
- 'unique_class_name_count' => 5,
- 'class_value_bytes' => 40,
- ),
- $this->summarize_class_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies resource inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_resource_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = 'One No href ';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 10,
- 'resource_tag_count' => 5,
- 'resource_attribute_count' => 5,
- 'unique_resource_tag_name_count' => 5,
- 'resource_value_bytes' => 38,
- ),
- $this->summarize_resource_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 7,
- 'resource_tag_count' => 5,
- 'resource_attribute_count' => 5,
- 'unique_resource_tag_name_count' => 5,
- 'resource_value_bytes' => 38,
- ),
- $this->summarize_resource_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies image inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_image_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = '
';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 5,
- 'image_count' => 3,
- 'src_count' => 2,
- 'alt_count' => 3,
- 'empty_alt_count' => 2,
- 'dimension_count' => 1,
- 'src_value_bytes' => 12,
- 'alt_value_bytes' => 3,
- ),
- $this->summarize_image_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 4,
- 'image_count' => 3,
- 'src_count' => 2,
- 'alt_count' => 3,
- 'empty_alt_count' => 2,
- 'dimension_count' => 1,
- 'src_value_bytes' => 12,
- 'alt_value_bytes' => 3,
- ),
- $this->summarize_image_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies script inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_script_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = ' ';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 5,
- 'script_count' => 3,
- 'src_count' => 2,
- 'module_count' => 1,
- 'async_count' => 1,
- 'defer_count' => 1,
- 'inline_script_bytes' => 10,
- 'src_value_bytes' => 17,
- ),
- $this->summarize_script_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 4,
- 'script_count' => 3,
- 'src_count' => 2,
- 'module_count' => 1,
- 'async_count' => 1,
- 'defer_count' => 1,
- 'inline_script_bytes' => 10,
- 'src_value_bytes' => 17,
- ),
- $this->summarize_script_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies form inventory summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_form_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $html = '';
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 7,
- 'form_count' => 1,
- 'control_count' => 4,
- 'named_control_count' => 3,
- 'unique_control_name_count' => 3,
- 'control_name_value_bytes' => 7,
- ),
- $this->summarize_form_inventory( $processor, true )
- );
-
- $processor = $this->create_tag_processor( $implementation, $html );
- $this->assertSame(
- array(
- 'tag_count' => 5,
- 'form_count' => 1,
- 'control_count' => 4,
- 'named_control_count' => 3,
- 'unique_control_name_count' => 3,
- 'control_name_value_bytes' => 7,
- ),
- $this->summarize_form_inventory( $processor, false )
- );
- }
-
- /**
- * Verifies chunked prefix summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_prefix_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
- $rows = array();
-
- do {
- $batch = $this->next_tag_prefix_summary_batch( $processor, $implementation, 'data-', 2, true );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertSame(
- array( 'MAIN', 'A', 'A', 'IMG', 'MAIN' ),
- array_column( $rows, 'tag_name' )
- );
- $this->assertSame(
- array( false, false, true, false, true ),
- array_column( $rows, 'is_tag_closer' )
- );
- $this->assertSame(
- array( 1, 1, 0, 1, 0 ),
- array_column( $rows, 'attribute_count' )
- );
- $this->assertSame( 5, count( $rows ) );
-
- if ( 'tag-processor' === $implementation ) {
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
- $this->assertSame(
- "MAIN\x1f0\x1f1\x1eA\x1f0\x1f1",
- $processor->next_tag_prefix_compact_summary_batch( 'data-', 2, true )
- );
- }
- }
-
- /**
- * Verifies chunked prefix count summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_prefix_count_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
- $summary = array(
- 'tag_count' => 0,
- 'attribute_count' => 0,
- );
-
- do {
- $batch = $this->next_tag_prefix_count_batch( $processor, 'data-', 2, true );
- if ( ! empty( $batch ) ) {
- $summary['tag_count'] += $batch['tag_count'];
- $summary['attribute_count'] += $batch['attribute_count'];
- }
- } while ( ! empty( $batch ) );
-
- $this->assertSame(
- array(
- 'tag_count' => 5,
- 'attribute_count' => 3,
- ),
- $summary
- );
- }
-
- /**
- * Verifies chunked tag summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_tag_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
- $rows = array();
-
- do {
- $batch = $this->next_tag_summary_batch( $processor, $implementation, 2, true );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertSame(
- array( 'MAIN', 'A', 'A', 'IMG', 'MAIN' ),
- array_column( $rows, 'tag_name' )
- );
- $this->assertSame(
- array( false, false, true, false, true ),
- array_column( $rows, 'is_tag_closer' )
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Link '
- );
-
- $this->assertSame(
- "MAIN\x1f0\x1eA\x1f0",
- $processor->next_tag_compact_summary_batch( 2, true )
- );
- }
-
- /**
- * Verifies chunked tag-name summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_matching_tag_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'One Two '
- );
- $rows = array();
-
- do {
- $batch = $this->next_matching_tag_summary_batch( $processor, $implementation, 'a', 2, true );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertSame(
- array( 'A', 'A', 'A', 'A' ),
- array_column( $rows, 'tag_name' )
- );
- $this->assertSame(
- array( false, true, false, true ),
- array_column( $rows, 'is_tag_closer' )
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'One Two '
- );
-
- $this->assertSame(
- "A\x1f0\x1eA\x1f1",
- $processor->next_matching_tag_compact_summary_batch( 'A', 2, true )
- );
- }
-
- /**
- * Verifies chunked tag-name and attribute summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_matching_tag_attribute_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'One Two No href '
- );
- $rows = array();
-
- do {
- $batch = $this->next_matching_tag_attribute_summary_batch( $processor, $implementation, 'a', 'href', 2, true );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertSame(
- array( 'A', 'A', 'A', 'A', 'A', 'A' ),
- array_column( $rows, 'tag_name' )
- );
- $this->assertSame(
- array( false, true, false, true, false, true ),
- array_column( $rows, 'is_tag_closer' )
- );
- $this->assertSame(
- array( '/one', null, '/two?x=1&y=2', null, null, null ),
- array_column( $rows, 'attribute_value' )
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'One Two '
- );
-
- $this->assertSame(
- "A\x1f0\x1f1/one\x1eA\x1f1\x1f0",
- $processor->next_matching_tag_attribute_compact_summary_batch( 'A', 'href', 2, true )
- );
- }
-
- /**
- * Verifies chunked tag-name and multi-attribute summaries match the per-tag workflow.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_matching_tag_attributes_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'One Two Three '
- );
- $rows = array();
-
- do {
- $batch = $this->next_matching_tag_attributes_summary_batch( $processor, $implementation, 'a', array( 'href', 'title', 'rel' ), 2, true );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertSame(
- array( 'A', 'A', 'A', 'A', 'A', 'A' ),
- array_column( $rows, 'tag_name' )
- );
- $this->assertSame(
- array( false, true, false, true, false, true ),
- array_column( $rows, 'is_tag_closer' )
- );
- $this->assertSame(
- array(
- array(
- 'href' => '/one',
- 'title' => 'One & two',
- 'rel' => null,
- ),
- array(
- 'href' => null,
- 'title' => null,
- 'rel' => null,
- ),
- array(
- 'href' => '/two',
- 'title' => null,
- 'rel' => '',
- ),
- array(
- 'href' => null,
- 'title' => null,
- 'rel' => null,
- ),
- array(
- 'href' => null,
- 'title' => 'Three',
- 'rel' => null,
- ),
- array(
- 'href' => null,
- 'title' => null,
- 'rel' => null,
- ),
- ),
- array_column( $rows, 'attribute_values' )
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'One Two '
- );
-
- $this->assertSame(
- "A\x1f0\x1f1/one\x1f1One\x1f0\x1eA\x1f1\x1f0\x1f0\x1f0",
- $this->next_matching_tag_attributes_compact_summary_batch( $processor, $implementation, 'A', array( 'href', 'title', 'rel' ), 2, true )
- );
- }
-
- /**
- * Verifies document-level tag-name and multi-attribute summaries.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_summarizes_matching_tag_attributes( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'One Two Three '
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 6,
- 'attribute_count' => 5,
- 'attribute_value_bytes' => 22,
- ),
- $this->summarize_matching_tag_attributes( $processor, $implementation, 'a', array( 'href', 'title', 'rel' ), true )
- );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'One Two Three '
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 3,
- 'attribute_count' => 5,
- 'attribute_value_bytes' => 22,
- ),
- $this->summarize_matching_tag_attributes( $processor, $implementation, 'A', array( 'href', 'title', 'rel' ), false )
- );
- }
-
- /**
- * Verifies numeric and legacy character-reference decoding parity.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_decodes_numeric_and_legacy_character_references( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- '¬in &x ©x '
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the anchor tag.' );
- $this->assertSame( "\u{fffd} \u{fffd} \u{20ac} \u{2026} A \u{00a9} ¬in &x ¬it;", $processor->get_attribute( 'title' ) );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the title tag.' );
- $this->assertSame( 'TITLE', $processor->get_token_name() );
- $this->assertSame( "\u{00ac}in &x \u{00a9}x \u{fffd}", $processor->get_modifiable_text() );
- }
-
- /**
- * Verifies the native tag processor exposes DOCTYPE tokens like PHP userland.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_doctype_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- foreach (
- array(
- '' => ' html',
- '' => '',
- '' => ' svg',
- '' => ' 123',
- '' => ' html' => ' html
Text
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the comment token.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertSame( 'note', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_HTML_COMMENT', $processor->get_comment_type() );
- $this->assertSame( 'note', $processor->get_full_comment_text() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip the comment and find the paragraph tag.' );
- $this->assertSame( 'P', $processor->get_tag() );
- $this->assertNull( $processor->get_comment_type() );
- $this->assertNull( $processor->get_full_comment_text() );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Text
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the comment token with a --!> close.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( 'note', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_HTML_COMMENT', $processor->get_comment_type() );
- $this->assertSame( 'note', $processor->get_full_comment_text() );
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to find the paragraph after the --!> comment.' );
- $this->assertSame( 'P', $processor->get_tag() );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'Text
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the presumptuous tag token.' );
- $this->assertSame( '#presumptuous-tag', $processor->get_token_type() );
- $this->assertSame( '#presumptuous-tag', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_modifiable_text() );
- $this->assertNull( $processor->get_full_comment_text() );
-
- foreach ( array( '1', '%bad', '/', ' p', '_x', ':x' ) as $expected_text ) {
- $this->assertTrue( $processor->next_token(), 'Expected a funky closing comment token.' );
- $this->assertSame( '#funky-comment', $processor->get_token_type() );
- $this->assertSame( '#funky-comment', $processor->get_token_name() );
- $this->assertSame( $expected_text, $processor->get_modifiable_text() );
- $this->assertSame( $expected_text, $processor->get_full_comment_text() );
- }
-
- $this->assertTrue( $processor->next_token(), 'Expected the abruptly closed HTML comment.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_ABRUPTLY_CLOSED_COMMENT', $processor->get_comment_type() );
- $this->assertSame( '', $processor->get_full_comment_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the dash-abruptly closed HTML comment.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_ABRUPTLY_CLOSED_COMMENT', $processor->get_comment_type() );
- $this->assertSame( '', $processor->get_full_comment_text() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip invalid tokens and find the paragraph tag.' );
- $this->assertSame( 'P', $processor->get_tag() );
- }
-
- /**
- * Verifies invalid opening tags remain text tokens like PHP userland.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_invalid_opening_tag_text_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'before & <1><%bad><_x><:x><.x><-x>< p>Text
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected an invalid opening-tag text token.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( '#text', $processor->get_token_name() );
- $this->assertSame( 'before & <1><%bad><_x><:x><.x><-x>< p>', $processor->get_modifiable_text() );
- $this->assertNull( $processor->get_comment_type() );
- $this->assertNull( $processor->get_full_comment_text() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip invalid text and find the paragraph tag.' );
- $this->assertSame( 'P', $processor->get_tag() );
-
- $processor = $this->create_tag_processor( $implementation, '<Text
<' );
-
- $this->assertTrue( $processor->next_token(), 'Expected adjacent invalid opening text.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( '<', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected valid paragraph after adjacent invalid opening text.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'P', $processor->get_tag() );
-
- $this->assertTrue( $processor->next_token(), 'Expected paragraph text.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( 'Text', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected paragraph closer.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'P', $processor->get_tag() );
- $this->assertTrue( $processor->is_tag_closer() );
-
- $this->assertFalse( $processor->next_token(), 'Expected a trailing lone < not to expose a token.' );
-
- $processor = $this->create_tag_processor( $implementation, 'xassertTrue( $processor->next_token(), 'Expected text before an incomplete tag-like sequence.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( 'x', $processor->get_modifiable_text() );
- $this->assertFalse( $processor->next_token(), 'Expected incomplete tag-like sequences not to expose tokens.' );
-
- $processor = $this->create_tag_processor( $implementation, '<< <Text
' );
-
- $this->assertTrue( $processor->next_token(), 'Expected consecutive invalid openings to coalesce.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( '<< <', $processor->get_modifiable_text() );
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to find the valid tag after consecutive invalid openings.' );
- $this->assertSame( 'P', $processor->get_tag() );
- }
-
- /**
- * Verifies raw-text contents are attached to the opening tag like PHP userland.
- *
- * @dataProvider data_tag_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_tag_processor_reads_raw_text_element_contents( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Tag_Processor' );
-
- $processor = $this->create_tag_processor(
- $implementation,
- 'D&E F&G H&I A&B © x
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the script token.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'SCRIPT', $processor->get_token_name() );
- $this->assertSame( 'if (a < b) { c(); }', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the iframe token after the script contents.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'IFRAME', $processor->get_token_name() );
- $this->assertSame( 'A&C ', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the noembed token after the iframe contents.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'NOEMBED', $processor->get_token_name() );
- $this->assertSame( 'D&E ', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the noframes token after the noembed contents.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'NOFRAMES', $processor->get_token_name() );
- $this->assertSame( 'F&G ', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the xmp token after the noframes contents.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'XMP', $processor->get_token_name() );
- $this->assertSame( 'H&I ', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the title token after the script contents.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'TITLE', $processor->get_token_name() );
- $this->assertSame( "A&B\u{00a0}\u{00a9}", $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the textarea token after the title contents.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'TEXTAREA', $processor->get_token_name() );
- $this->assertSame( "Cget_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the paragraph token after the script contents.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'P', $processor->get_token_name() );
- $this->assertFalse( $processor->is_tag_closer(), 'The script closer should not be exposed as the next token.' );
- }
-
- /**
- * Verifies the first native slice matches the PHP HTML processor token stream.
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_reads_tag_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $processor = $this->create_html_processor(
- $implementation,
- ''
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the section token.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'SECTION', $processor->get_token_name() );
- $this->assertFalse( $processor->is_tag_closer() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the paragraph token.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'P', $processor->get_token_name() );
- $this->assertFalse( $processor->is_tag_closer() );
- $this->assertSame( '7', $processor->get_attribute( 'data-id' ) );
-
- while ( $processor->next_token() && ! $processor->is_tag_closer() ) {
- continue;
- }
-
- $this->assertTrue( $processor->is_tag_closer(), 'next_token() should expose closing tag tokens.' );
- $this->assertSame( 'P', $processor->get_token_name() );
- }
-
- /**
- * Verifies the first native slice matches PHP HTML processor ancestry APIs.
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_reports_breadcrumbs_and_current_depth( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $processor = $this->create_html_processor(
- $implementation,
- ''
- );
-
- $this->assertSame( 2, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the section token.' );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', 'SECTION' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the paragraph token.' );
- $this->assertSame( 4, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', 'SECTION', 'P' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the image token.' );
- $this->assertSame( 5, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', 'SECTION', 'P', 'IMG' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the paragraph closer.' );
- $this->assertTrue( $processor->is_tag_closer(), 'Expected a closing paragraph token.' );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', 'SECTION' ), $processor->get_breadcrumbs() );
- }
-
- /**
- * Verifies text and comment tokens are visible through next_token().
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_reads_text_and_comment_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $processor = $this->create_html_processor(
- $implementation,
- 'HelloWorld
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the paragraph token.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'P', $processor->get_token_name() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the text token.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( '#text', $processor->get_token_name() );
- $this->assertSame( 'Hello', $processor->get_modifiable_text() );
- $this->assertSame( 4, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', 'P', '#text' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the comment token.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertSame( 'note', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_HTML_COMMENT', $processor->get_comment_type() );
- $this->assertSame( 'note', $processor->get_full_comment_text() );
- $this->assertSame( 4, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', 'P', '#comment' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the emphasis token.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'EM', $processor->get_token_name() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the nested text token.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( 'World', $processor->get_modifiable_text() );
- $this->assertSame( 5, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', 'P', 'EM', '#text' ), $processor->get_breadcrumbs() );
- }
-
- /**
- * Verifies chunked HTML processor token summaries match next_token() metadata.
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_reads_token_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $processor = $this->create_html_processor(
- $implementation,
- ''
- );
- $rows = array();
-
- do {
- $batch = $this->next_html_token_summary_batch( $processor, $implementation, 2 );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertSame(
- array( '#tag', '#tag', '#text', '#tag', '#comment', '#tag' ),
- array_column( $rows, 'token_type' )
- );
- $this->assertSame(
- array( 'SECTION', 'P', '#text', 'P', '#comment', 'SECTION' ),
- array_column( $rows, 'token_name' )
- );
- $this->assertSame(
- array( false, false, false, true, false, true ),
- array_column( $rows, 'is_tag_closer' )
- );
- $this->assertSame(
- array( 3, 4, 5, 3, 4, 2 ),
- array_column( $rows, 'current_depth' )
- );
- $this->assertSame(
- array( 'HTML', 'BODY', 'SECTION', 'P', '#text' ),
- $rows[2]['breadcrumbs']
- );
-
- $processor = $this->create_html_processor(
- $implementation,
- ''
- );
-
- $this->assertSame(
- "t\x1fSECTION\x1f0\x1f3\x1fHTML\x1dBODY\x1dSECTION\x1et\x1fP\x1f0\x1f4\x1fHTML\x1dBODY\x1dSECTION\x1dP",
- $processor->next_token_compact_summary_batch( 2 )
- );
- }
-
- /**
- * Verifies non-standard comment-like tokens stay visible in fragment parsing.
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_reads_funky_comment_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $processor = $this->create_html_processor(
- $implementation,
- '1 data?>Text
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the processing-instruction-looking comment.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertSame( 'pi.name', $processor->get_tag() );
- $this->assertSame( ' data', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_PI_NODE_LOOKALIKE', $processor->get_comment_type() );
- $this->assertSame( '?pi.name data?', $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#comment' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the invalid processing-instruction-looking comment with a numeric target.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertNull( $processor->get_tag() );
- $this->assertSame( '1 data?', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_INVALID_HTML', $processor->get_comment_type() );
- $this->assertSame( '?1 data?', $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#comment' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the invalid processing-instruction-looking comment without XML-style close.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertNull( $processor->get_tag() );
- $this->assertSame( 'pi data', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_INVALID_HTML', $processor->get_comment_type() );
- $this->assertSame( '?pi data', $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#comment' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the CDATA-looking comment.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertNull( $processor->get_tag() );
- $this->assertSame( 'x', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_CDATA_LOOKALIKE', $processor->get_comment_type() );
- $this->assertSame( '[CDATA[x]]', $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#comment' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the invalid-HTML comment.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertNull( $processor->get_tag() );
- $this->assertSame( 'notdoctype', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_INVALID_HTML', $processor->get_comment_type() );
- $this->assertSame( 'notdoctype', $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#comment' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip funky comments and find the paragraph tag.' );
- $this->assertSame( 'P', $processor->get_token_name() );
- }
-
- /**
- * Verifies processor handling of invalid closing and abruptly closed comment tokens.
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_reads_invalid_closing_and_abrupt_comment_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $processor = $this->create_html_processor(
- $implementation,
- '>1>%bad>/> p>Text
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the presumptuous tag token.' );
- $this->assertSame( '#presumptuous-tag', $processor->get_token_type() );
- $this->assertSame( '#presumptuous-tag', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_modifiable_text() );
- $this->assertNull( $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#presumptuous-tag' ), $processor->get_breadcrumbs() );
-
- foreach ( array( '1', '%bad', '/', ' p', '_x', ':x' ) as $expected_text ) {
- $this->assertTrue( $processor->next_token(), 'Expected a funky closing comment token.' );
- $this->assertSame( '#funky-comment', $processor->get_token_type() );
- $this->assertSame( '#funky-comment', $processor->get_token_name() );
- $this->assertSame( $expected_text, $processor->get_modifiable_text() );
- $this->assertSame( $expected_text, $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#funky-comment' ), $processor->get_breadcrumbs() );
- }
-
- $this->assertTrue( $processor->next_token(), 'Expected the abruptly closed HTML comment.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_ABRUPTLY_CLOSED_COMMENT', $processor->get_comment_type() );
- $this->assertSame( '', $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#comment' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the dash-abruptly closed HTML comment.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_modifiable_text() );
- $this->assertSame( 'COMMENT_AS_ABRUPTLY_CLOSED_COMMENT', $processor->get_comment_type() );
- $this->assertSame( '', $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#comment' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip invalid tokens and find the paragraph tag.' );
- $this->assertSame( 'P', $processor->get_token_name() );
- }
-
- /**
- * Verifies invalid opening tags stay visible as text in fragment parsing.
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_reads_invalid_opening_tag_text_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $processor = $this->create_html_processor(
- $implementation,
- 'before & <1><%bad><_x><:x><.x><-x>< p>Text
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected an invalid opening-tag text token.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( '#text', $processor->get_token_name() );
- $this->assertSame( 'before & <1><%bad><_x><:x><.x><-x>< p>', $processor->get_modifiable_text() );
- $this->assertNull( $processor->get_comment_type() );
- $this->assertNull( $processor->get_full_comment_text() );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame( array( 'HTML', 'BODY', '#text' ), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip invalid text and find the paragraph tag.' );
- $this->assertSame( 'P', $processor->get_token_name() );
-
- $processor = $this->create_html_processor( $implementation, '<Text
<' );
-
- $this->assertTrue( $processor->next_token(), 'Expected adjacent invalid opening text.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( '<', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected valid paragraph after adjacent invalid opening text.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'P', $processor->get_token_name() );
-
- $this->assertTrue( $processor->next_token(), 'Expected paragraph text.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( 'Text', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected paragraph closer.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'P', $processor->get_token_name() );
- $this->assertTrue( $processor->is_tag_closer() );
-
- $this->assertFalse( $processor->next_token(), 'Expected a trailing lone < not to expose a token.' );
-
- $processor = $this->create_html_processor( $implementation, 'xassertTrue( $processor->next_token(), 'Expected text before an incomplete tag-like sequence.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( 'x', $processor->get_modifiable_text() );
- $this->assertFalse( $processor->next_token(), 'Expected incomplete tag-like sequences not to expose tokens.' );
-
- $processor = $this->create_html_processor( $implementation, '<< <Text
' );
-
- $this->assertTrue( $processor->next_token(), 'Expected consecutive invalid openings to coalesce.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( '<< <', $processor->get_modifiable_text() );
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to find the valid tag after consecutive invalid openings.' );
- $this->assertSame( 'P', $processor->get_token_name() );
- }
-
- /**
- * Verifies table, list, option, and paragraph fragments synthesize implied tokens.
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_synthesizes_selected_implied_closers( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $cases = array(
- 'table-cells' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-bare-cells' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-explicit-sections' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'THEAD', false, array( 'HTML', 'BODY', 'TABLE', 'THEAD' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'THEAD', 'TR' ) ),
- array( 'TH', false, array( 'HTML', 'BODY', 'TABLE', 'THEAD', 'TR', 'TH' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'THEAD', 'TR', 'TH', '#text' ) ),
- array( 'TH', true, array( 'HTML', 'BODY', 'TABLE', 'THEAD', 'TR' ) ),
- array( 'TH', false, array( 'HTML', 'BODY', 'TABLE', 'THEAD', 'TR', 'TH' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'THEAD', 'TR', 'TH', '#text' ) ),
- array( 'TH', true, array( 'HTML', 'BODY', 'TABLE', 'THEAD', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'THEAD' ) ),
- array( 'THEAD', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-repeated-body-sections' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-colgroup' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'COLGROUP', false, array( 'HTML', 'BODY', 'TABLE', 'COLGROUP' ) ),
- array( 'COL', false, array( 'HTML', 'BODY', 'TABLE', 'COLGROUP', 'COL' ) ),
- array( 'COLGROUP', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-caption' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'CAPTION', false, array( 'HTML', 'BODY', 'TABLE', 'CAPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'CAPTION', '#text' ) ),
- array( 'CAPTION', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-caption-after-row' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'CAPTION', false, array( 'HTML', 'BODY', 'TABLE', 'CAPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'CAPTION', '#text' ) ),
- array( 'CAPTION', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-caption-after-colgroup' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'COLGROUP', false, array( 'HTML', 'BODY', 'TABLE', 'COLGROUP' ) ),
- array( 'COL', false, array( 'HTML', 'BODY', 'TABLE', 'COLGROUP', 'COL' ) ),
- array( 'COLGROUP', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'CAPTION', false, array( 'HTML', 'BODY', 'TABLE', 'CAPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'CAPTION', '#text' ) ),
- array( 'CAPTION', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-stray-row-closer-ignored' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-stray-caption-closer-ignored' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-unsupported-stray-closer-aborts-table' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- ),
- ),
- 'table-template-stray-closer-ignored' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-form-before-row' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'FORM', false, array( 'HTML', 'BODY', 'TABLE', 'FORM' ) ),
- array( 'FORM', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-explicit-form-before-row' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'FORM', false, array( 'HTML', 'BODY', 'TABLE', 'FORM' ) ),
- array( 'FORM', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- ),
- ),
- 'table-form-whitespace-before-row' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'FORM', false, array( 'HTML', 'BODY', 'TABLE', 'FORM' ) ),
- array( 'FORM', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', '#text' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-form-text-aborts-table' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'FORM', false, array( 'HTML', 'BODY', 'TABLE', 'FORM' ) ),
- array( 'FORM', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- ),
- ),
- 'table-form-comment-before-row' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'FORM', false, array( 'HTML', 'BODY', 'TABLE', 'FORM' ) ),
- array( 'FORM', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( '#comment', false, array( 'HTML', 'BODY', 'TABLE', '#comment' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-form-script-before-row' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'FORM', false, array( 'HTML', 'BODY', 'TABLE', 'FORM' ) ),
- array( 'FORM', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'SCRIPT', false, array( 'HTML', 'BODY', 'TABLE', 'SCRIPT' ) ),
- array( 'TBODY', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TR', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TD', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR', 'TD', '#text' ) ),
- array( 'TD', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY', 'TR' ) ),
- array( 'TR', true, array( 'HTML', 'BODY', 'TABLE', 'TBODY' ) ),
- array( 'TBODY', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'TABLE', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-form-flow-start-aborts-table' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- array( 'FORM', false, array( 'HTML', 'BODY', 'TABLE', 'FORM' ) ),
- array( 'FORM', true, array( 'HTML', 'BODY', 'TABLE' ) ),
- ),
- ),
- 'table-flow-start-aborts-table' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- ),
- ),
- 'table-inline-start-aborts-table' => array(
- '',
- array(
- array( 'TABLE', false, array( 'HTML', 'BODY', 'TABLE' ) ),
- ),
- ),
- 'list-items' => array(
- '',
- array(
- array( 'UL', false, array( 'HTML', 'BODY', 'UL' ) ),
- array( 'LI', false, array( 'HTML', 'BODY', 'UL', 'LI' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'UL', 'LI', '#text' ) ),
- array( 'LI', true, array( 'HTML', 'BODY', 'UL' ) ),
- array( 'LI', false, array( 'HTML', 'BODY', 'UL', 'LI' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'UL', 'LI', '#text' ) ),
- array( 'LI', true, array( 'HTML', 'BODY', 'UL' ) ),
- array( 'UL', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'ordinary-eof-closers' => array(
- 'Text',
- array(
- array( 'DIV', false, array( 'HTML', 'BODY', 'DIV' ) ),
- array( 'SPAN', false, array( 'HTML', 'BODY', 'DIV', 'SPAN' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'DIV', 'SPAN', '#text' ) ),
- array( 'SPAN', true, array( 'HTML', 'BODY', 'DIV' ) ),
- array( 'DIV', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'void-child-eof-closer' => array(
- ' ',
- array(
- array( 'A', false, array( 'HTML', 'BODY', 'A' ) ),
- array( 'IMG', false, array( 'HTML', 'BODY', 'A', 'IMG' ) ),
- array( 'A', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'options' => array(
- 'A B ',
- array(
- array( 'SELECT', false, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'OPTION', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION', '#text' ) ),
- array( 'OPTION', true, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'OPTION', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION', '#text' ) ),
- array( 'OPTION', true, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'SELECT', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'paragraphs' => array(
- 'One
Two',
- array(
- array( 'P', false, array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'P', true, array( 'HTML', 'BODY' ) ),
- array( 'P', false, array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'P', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'paragraph-before-block' => array(
- '
Text
Block
',
- array(
- array( 'P', false, array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'P', true, array( 'HTML', 'BODY' ) ),
- array( 'DIV', false, array( 'HTML', 'BODY', 'DIV' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'DIV', '#text' ) ),
- array( 'DIV', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'paragraph-before-heading' => array(
- 'Text
Heading ',
- array(
- array( 'P', false, array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'P', true, array( 'HTML', 'BODY' ) ),
- array( 'H1', false, array( 'HTML', 'BODY', 'H1' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'H1', '#text' ) ),
- array( 'H1', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'paragraph-before-inline' => array(
- 'TextInline ',
- array(
- array( 'P', false, array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'SPAN', false, array( 'HTML', 'BODY', 'P', 'SPAN' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'P', 'SPAN', '#text' ) ),
- array( 'SPAN', true, array( 'HTML', 'BODY', 'P' ) ),
- array( 'P', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'heading-before-heading' => array(
- '
OneTwo ',
- array(
- array( 'H1', false, array( 'HTML', 'BODY', 'H1' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'H1', '#text' ) ),
- array( 'H1', true, array( 'HTML', 'BODY' ) ),
- array( 'H2', false, array( 'HTML', 'BODY', 'H2' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'H2', '#text' ) ),
- array( 'H2', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'heading-before-inline' => array(
- 'OneSpan ',
- array(
- array( 'H1', false, array( 'HTML', 'BODY', 'H1' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'H1', '#text' ) ),
- array( 'SPAN', false, array( 'HTML', 'BODY', 'H1', 'SPAN' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'H1', 'SPAN', '#text' ) ),
- array( 'SPAN', true, array( 'HTML', 'BODY', 'H1' ) ),
- array( 'H1', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'button-before-button' => array(
- 'OneTwo ',
- array(
- array( 'BUTTON', false, array( 'HTML', 'BODY', 'BUTTON' ) ),
- array( 'SPAN', false, array( 'HTML', 'BODY', 'BUTTON', 'SPAN' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'BUTTON', 'SPAN', '#text' ) ),
- array( 'SPAN', true, array( 'HTML', 'BODY', 'BUTTON' ) ),
- array( 'BUTTON', true, array( 'HTML', 'BODY' ) ),
- array( 'BUTTON', false, array( 'HTML', 'BODY', 'BUTTON' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'BUTTON', '#text' ) ),
- array( 'BUTTON', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'button-explicit-closer' => array(
- 'One ',
- array(
- array( 'BUTTON', false, array( 'HTML', 'BODY', 'BUTTON' ) ),
- array( 'SPAN', false, array( 'HTML', 'BODY', 'BUTTON', 'SPAN' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'BUTTON', 'SPAN', '#text' ) ),
- array( 'SPAN', true, array( 'HTML', 'BODY', 'BUTTON' ) ),
- array( 'BUTTON', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'summary-before-details-closer' => array(
- 'OneTwo ',
- array(
- array( 'DETAILS', false, array( 'HTML', 'BODY', 'DETAILS' ) ),
- array( 'SUMMARY', false, array( 'HTML', 'BODY', 'DETAILS', 'SUMMARY' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'DETAILS', 'SUMMARY', '#text' ) ),
- array( 'SUMMARY', false, array( 'HTML', 'BODY', 'DETAILS', 'SUMMARY', 'SUMMARY' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'DETAILS', 'SUMMARY', 'SUMMARY', '#text' ) ),
- array( 'SUMMARY', true, array( 'HTML', 'BODY', 'DETAILS', 'SUMMARY' ) ),
- array( 'SUMMARY', true, array( 'HTML', 'BODY', 'DETAILS' ) ),
- array( 'DETAILS', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'nested-form-start-ignored' => array(
- '',
- array(
- array( 'FORM', false, array( 'HTML', 'BODY', 'FORM' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'FORM', '#text' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'FORM', '#text' ) ),
- array( 'FORM', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'nobr-before-nobr' => array(
- 'OneTwo ',
- array(
- array( 'NOBR', false, array( 'HTML', 'BODY', 'NOBR' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'NOBR', '#text' ) ),
- array( 'NOBR', true, array( 'HTML', 'BODY' ) ),
- array( 'NOBR', false, array( 'HTML', 'BODY', 'NOBR' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'NOBR', '#text' ) ),
- array( 'NOBR', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'nobr-explicit-closer' => array(
- 'One ',
- array(
- array( 'NOBR', false, array( 'HTML', 'BODY', 'NOBR' ) ),
- array( 'SPAN', false, array( 'HTML', 'BODY', 'NOBR', 'SPAN' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'NOBR', 'SPAN', '#text' ) ),
- array( 'SPAN', true, array( 'HTML', 'BODY', 'NOBR' ) ),
- array( 'NOBR', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'select-before-input' => array(
- 'A B ',
- array(
- array( 'SELECT', false, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'OPTION', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION', '#text' ) ),
- array( 'OPTION', true, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'SELECT', true, array( 'HTML', 'BODY' ) ),
- array( 'INPUT', false, array( 'HTML', 'BODY', 'INPUT' ) ),
- array( '#text', false, array( 'HTML', 'BODY', '#text' ) ),
- ),
- ),
- 'select-option-before-hr' => array(
- 'A B ',
- array(
- array( 'SELECT', false, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'OPTION', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION', '#text' ) ),
- array( 'OPTION', true, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'HR', false, array( 'HTML', 'BODY', 'SELECT', 'HR' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', '#text' ) ),
- array( 'SELECT', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'select-before-textarea' => array(
- 'A ',
- array(
- array( 'SELECT', false, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'OPTION', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION', '#text' ) ),
- array( 'OPTION', true, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'SELECT', true, array( 'HTML', 'BODY' ) ),
- array( 'TEXTAREA', false, array( 'HTML', 'BODY', 'TEXTAREA' ) ),
- ),
- ),
- 'html-and-body-starts-ignored' => array(
- 'x',
- array(
- array( 'P', false, array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'P', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'description-list' => array(
- '
A B C ',
- array(
- array( 'DL', false, array( 'HTML', 'BODY', 'DL' ) ),
- array( 'DT', false, array( 'HTML', 'BODY', 'DL', 'DT' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'DL', 'DT', '#text' ) ),
- array( 'DT', true, array( 'HTML', 'BODY', 'DL' ) ),
- array( 'DD', false, array( 'HTML', 'BODY', 'DL', 'DD' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'DL', 'DD', '#text' ) ),
- array( 'DD', true, array( 'HTML', 'BODY', 'DL' ) ),
- array( 'DT', false, array( 'HTML', 'BODY', 'DL', 'DT' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'DL', 'DT', '#text' ) ),
- array( 'DT', true, array( 'HTML', 'BODY', 'DL' ) ),
- array( 'DL', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'ruby-text' => array(
- 'ab(c ',
- array(
- array( 'RUBY', false, array( 'HTML', 'BODY', 'RUBY' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'RUBY', '#text' ) ),
- array( 'RT', false, array( 'HTML', 'BODY', 'RUBY', 'RT' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'RUBY', 'RT', '#text' ) ),
- array( 'RT', true, array( 'HTML', 'BODY', 'RUBY' ) ),
- array( 'RP', false, array( 'HTML', 'BODY', 'RUBY', 'RP' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'RUBY', 'RP', '#text' ) ),
- array( 'RP', true, array( 'HTML', 'BODY', 'RUBY' ) ),
- array( 'RT', false, array( 'HTML', 'BODY', 'RUBY', 'RT' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'RUBY', 'RT', '#text' ) ),
- array( 'RT', true, array( 'HTML', 'BODY', 'RUBY' ) ),
- array( 'RUBY', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'optgroups' => array(
- 'A B C ',
- array(
- array( 'SELECT', false, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'OPTION', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', 'OPTION', '#text' ) ),
- array( 'OPTION', true, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'OPTGROUP', false, array( 'HTML', 'BODY', 'SELECT', 'OPTGROUP' ) ),
- array( 'OPTION', false, array( 'HTML', 'BODY', 'SELECT', 'OPTGROUP', 'OPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', 'OPTGROUP', 'OPTION', '#text' ) ),
- array( 'OPTION', true, array( 'HTML', 'BODY', 'SELECT', 'OPTGROUP' ) ),
- array( 'OPTGROUP', true, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'OPTGROUP', false, array( 'HTML', 'BODY', 'SELECT', 'OPTGROUP' ) ),
- array( 'OPTION', false, array( 'HTML', 'BODY', 'SELECT', 'OPTGROUP', 'OPTION' ) ),
- array( '#text', false, array( 'HTML', 'BODY', 'SELECT', 'OPTGROUP', 'OPTION', '#text' ) ),
- array( 'OPTION', true, array( 'HTML', 'BODY', 'SELECT', 'OPTGROUP' ) ),
- array( 'OPTGROUP', true, array( 'HTML', 'BODY', 'SELECT' ) ),
- array( 'SELECT', true, array( 'HTML', 'BODY' ) ),
- ),
- ),
- );
-
- foreach ( $cases as $label => $case ) {
- $processor = $this->create_html_processor( $implementation, $case[0] );
-
- foreach ( $case[1] as $expected ) {
- $this->assertTrue( $processor->next_token(), "Expected {$label} token." );
- $this->assertSame( $expected[0], $processor->get_token_name(), "Unexpected {$label} token name." );
- $this->assertSame( $expected[1], $processor->is_tag_closer(), "Unexpected {$label} closer flag." );
- $this->assertSame( $expected[2], $processor->get_breadcrumbs(), "Unexpected {$label} breadcrumbs." );
- }
-
- $this->assertFalse( $processor->next_token(), "Expected {$label} to be exhausted." );
- }
- }
-
- /**
- * Verifies HTML processor text-token subdivision and table-text boundaries.
- *
- * @dataProvider data_html_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_html_processor_subdivides_selected_text_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation, 'WP_HTML_Native_Processor' );
-
- $cases = array(
- 'body-text-leading-whitespace' => array(
- ' A B
',
- array(
- array( 'P', '', array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', ' ', array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( '#text', 'A ', array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'B', '', array( 'HTML', 'BODY', 'P', 'B' ) ),
- array( '#text', 'B', array( 'HTML', 'BODY', 'P', 'B', '#text' ) ),
- array( 'B', '', array( 'HTML', 'BODY', 'P' ) ),
- array( 'P', '', array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'body-text-reference-leading-whitespace' => array(
- '
A
',
- array(
- array( 'P', '', array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', " \n ", array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( '#text', 'A', array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'P', '', array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'body-text-raw-null' => array(
- "\0 A\0B
",
- array(
- array( 'P', '', array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', ' ', array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( '#text', 'AB', array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'P', '', array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'body-text-newlines' => array(
- "\r\n\tA\rB
",
- array(
- array( 'P', '', array( 'HTML', 'BODY', 'P' ) ),
- array( '#text', "\n\t", array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( '#text', "A\nB", array( 'HTML', 'BODY', 'P', '#text' ) ),
- array( 'P', '', array( 'HTML', 'BODY' ) ),
- ),
- ),
- 'table-text-leading-whitespace' => array(
- '',
- array(
- array( 'TABLE', '', array( 'HTML', 'BODY', 'TABLE' ) ),
- array( '#text', ' ', array( 'HTML', 'BODY', 'TABLE', '#text' ) ),
- ),
- ),
- 'table-text-reference-leading-whitespace' => array(
- '',
- array(
- array( 'TABLE', '', array( 'HTML', 'BODY', 'TABLE' ) ),
- array( '#text', ' ', array( 'HTML', 'BODY', 'TABLE', '#text' ) ),
- ),
- ),
- 'table-text-no-leading-whitespace' => array(
- '',
- array(
- array( 'TABLE', '', array( 'HTML', 'BODY', 'TABLE' ) ),
- ),
- ),
- );
-
- foreach ( $cases as $label => $case ) {
- $processor = $this->create_html_processor( $implementation, $case[0] );
-
- foreach ( $case[1] as $expected ) {
- $this->assertTrue( $processor->next_token(), "Expected {$label} token." );
- $this->assertSame( $expected[0], $processor->get_token_name(), "Unexpected {$label} token name." );
- $this->assertSame( $expected[1], $processor->get_modifiable_text(), "Unexpected {$label} text." );
- $this->assertSame( $expected[2], $processor->get_breadcrumbs(), "Unexpected {$label} breadcrumbs." );
- }
-
- $this->assertFalse( $processor->next_token(), "Expected {$label} to be exhausted." );
- }
- }
-
- /**
- * Data provider.
- *
- * @return array[]
- */
- public static function data_tag_processor_implementations() {
- return array(
- 'php-tag-processor' => array( 'php-tag-processor' ),
- 'native-tag-processor' => array( 'native-tag-processor' ),
- );
- }
-
- /**
- * Data provider.
- *
- * @return array[]
- */
- public static function data_html_processor_implementations() {
- return array(
- 'php-html-processor' => array( 'php-html-processor' ),
- 'native-html-processor' => array( 'native-html-processor' ),
- );
- }
-
- /**
- * Creates a tag processor for a specific implementation.
- *
- * @param string $implementation Implementation identifier.
- * @param string $html HTML input.
- * @return object Processor instance.
- */
- private function create_tag_processor( $implementation, $html ) {
- if ( 'native-tag-processor' === $implementation ) {
- return new WP_HTML_Native_Tag_Processor( $html );
- }
-
- $processor = new WP_HTML_Tag_Processor( $html );
- $this->disable_native_delegate( $processor );
-
- return $processor;
- }
-
- /**
- * Creates an HTML processor for a specific implementation.
- *
- * @param string $implementation Implementation identifier.
- * @param string $html HTML input.
- * @return object Processor instance.
- */
- private function create_html_processor( $implementation, $html ) {
- if ( 'native-html-processor' === $implementation ) {
- return WP_HTML_Native_Processor::create_fragment( $html );
- }
-
- $processor = WP_HTML_Processor::create_fragment( $html );
- $this->disable_native_delegate( $processor );
-
- return $processor;
- }
-
- /**
- * Disables the native delegate so PHP implementation rows still cover PHP.
- *
- * @param object $processor Processor instance.
- */
- private function disable_native_delegate( $processor ) {
- if ( ! property_exists( 'WP_HTML_Tag_Processor', 'native_processor' ) ) {
- return;
- }
-
- $property = new ReflectionProperty( 'WP_HTML_Tag_Processor', 'native_processor' );
- $property->setAccessible( true );
- $property->setValue( $processor, null );
- }
-
- /**
- * Returns the native delegate for public-class default checks.
- *
- * @param object $processor Processor instance.
- * @return object|null Native delegate.
- */
- private function get_native_delegate( $processor ) {
- if ( ! property_exists( 'WP_HTML_Tag_Processor', 'native_processor' ) ) {
- return null;
- }
-
- $property = new ReflectionProperty( 'WP_HTML_Tag_Processor', 'native_processor' );
- $property->setAccessible( true );
-
- return $property->getValue( $processor );
- }
-
- /**
- * Runs a fresh PHP process that defines native-default constants before bootstrap.
- *
- * @param string $constant_definitions PHP source defining constants.
- * @return string[] Probe output lines.
- */
- private function run_html_native_defaults_constant_probe( $constant_definitions ) {
- $root = dirname( __DIR__, 3 );
- $extension = $root . '/extensions/native-apis/target/release/libwp_native_apis.so';
-
- if ( ! file_exists( $extension ) ) {
- $this->markTestSkipped( 'Native API extension binary is not built.' );
- }
-
- $code = $constant_definitions . "\n" .
- 'require ' . var_export( $root . '/bootstrap.php', true ) . ";\n" .
- '$property = new ReflectionProperty( \'WP_HTML_Tag_Processor\', \'native_processor\' );' . "\n" .
- '$property->setAccessible( true );' . "\n" .
- '$tag_processor = new WP_HTML_Tag_Processor( \'Text
\' );' . "\n" .
- '$tag_delegate = $property->getValue( $tag_processor );' . "\n" .
- 'echo \'tag:\' . ( null === $tag_delegate ? \'php\' : get_class( $tag_delegate ) ) . "\n";' . "\n" .
- '$tag_processor->next_tag( \'p\' );' . "\n" .
- 'echo \'tag-id:\' . $tag_processor->get_attribute( \'data-id\' ) . "\n";' . "\n" .
- '$html_processor = WP_HTML_Processor::create_fragment( \'Text
\' );' . "\n" .
- '$tree_delegate = $property->getValue( $html_processor );' . "\n" .
- 'echo \'tree:\' . ( null === $tree_delegate ? \'php\' : get_class( $tree_delegate ) ) . "\n";' . "\n" .
- '$html_processor->next_token();' . "\n" .
- 'echo \'tree-token:\' . $html_processor->get_token_name() . "\n";' . "\n" .
- '$full_processor = WP_HTML_Processor::create_full_parser( \'Text \' );' . "\n" .
- '$full_delegate = $property->getValue( $full_processor );' . "\n" .
- 'echo \'full:\' . ( null === $full_delegate ? \'php\' : get_class( $full_delegate ) ) . "\n";' . "\n" .
- '$full_processor->next_token();' . "\n" .
- 'echo \'full-token:\' . $full_processor->get_token_name() . "\n";';
-
- $command = escapeshellarg( PHP_BINARY ) . ' -d extension=' . escapeshellarg( $extension ) . ' -r ' . escapeshellarg( $code );
- $output = array();
- $status = 0;
- exec( $command . ' 2>&1', $output, $status );
-
- $this->assertSame( 0, $status, implode( "\n", $output ) );
-
- return $output;
- }
-
- /**
- * Returns the public parser state for lifecycle conformance checks.
- *
- * @param object $processor Processor instance.
- * @return string Parser state.
- */
- private function get_parser_state( $processor ) {
- $property = new ReflectionProperty( 'WP_HTML_Tag_Processor', 'parser_state' );
- $property->setAccessible( true );
-
- return $property->getValue( $processor );
- }
-
- /**
- * Advances either tag-processor implementation while visiting closers.
- *
- * @param object $processor Processor instance.
- * @return bool Whether a tag was matched.
- */
- private function next_tag_visiting_closers( $processor ) {
- if ( method_exists( $processor, 'next_tag_any' ) ) {
- return $processor->next_tag_any( true, 1 );
- }
-
- return $processor->next_tag( array( 'tag_closers' => 'visit' ) );
- }
-
- /**
- * Skips implementation rows when an API is provided only by the native class.
- *
- * @param object $processor Processor instance.
- * @param string $method Method name.
- */
- private function skip_if_html_method_is_unavailable( $processor, $method ) {
- if ( ! method_exists( $processor, $method ) ) {
- $this->markTestSkipped( "{$method} is not exposed by this HTML processor implementation." );
- }
- }
-
- /**
- * Summarizes prefixed attributes for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $prefix Attribute-name prefix.
- * @param bool $closers Whether to include closing tags in the tag count.
- * @return array Summary with `tag_count` and `attribute_count`.
- */
- private function summarize_attribute_names_with_prefix( $processor, $prefix, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_attribute_names_with_prefix' );
-
- $summary = $processor->summarize_attribute_names_with_prefix( $prefix, $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 2 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'attribute_count' => (int) $parts[1],
- );
- }
-
- /**
- * Summarizes tag inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with tag inventory counts.
- */
- private function summarize_tag_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_tag_inventory' );
-
- $summary = $processor->summarize_tag_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 5 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'open_tag_count' => (int) $parts[1],
- 'closing_tag_count' => (int) $parts[2],
- 'attribute_count' => (int) $parts[3],
- 'unique_tag_name_count' => (int) $parts[4],
- );
- }
-
- /**
- * Summarizes heading inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with heading inventory counts.
- */
- private function summarize_heading_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_heading_inventory' );
-
- $summary = $processor->summarize_heading_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 8 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'heading_count' => (int) $parts[1],
- 'h1_count' => (int) $parts[2],
- 'h2_count' => (int) $parts[3],
- 'h3_count' => (int) $parts[4],
- 'h4_count' => (int) $parts[5],
- 'h5_count' => (int) $parts[6],
- 'h6_count' => (int) $parts[7],
- );
- }
-
- /**
- * Summarizes ID inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with ID inventory counts.
- */
- private function summarize_id_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_id_inventory' );
-
- $summary = $processor->summarize_id_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 5 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'id_tag_count' => (int) $parts[1],
- 'unique_id_count' => (int) $parts[2],
- 'duplicate_id_count' => (int) $parts[3],
- 'id_value_bytes' => (int) $parts[4],
- );
- }
-
- /**
- * Summarizes attribute inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with attribute inventory counts.
- */
- private function summarize_attribute_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_attribute_inventory' );
-
- $summary = $processor->summarize_attribute_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 4 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'attribute_count' => (int) $parts[1],
- 'unique_attribute_name_count' => (int) $parts[2],
- 'attribute_value_bytes' => (int) $parts[3],
- );
- }
-
- /**
- * Summarizes data-attribute inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with data-attribute inventory counts.
- */
- private function summarize_data_attribute_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_data_attribute_inventory' );
-
- $summary = $processor->summarize_data_attribute_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 5 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'data_attribute_tag_count' => (int) $parts[1],
- 'data_attribute_count' => (int) $parts[2],
- 'unique_data_attribute_name_count' => (int) $parts[3],
- 'data_attribute_value_bytes' => (int) $parts[4],
- );
- }
-
- /**
- * Summarizes ARIA attribute inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with ARIA attribute inventory counts.
- */
- private function summarize_aria_attribute_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_aria_attribute_inventory' );
-
- $summary = $processor->summarize_aria_attribute_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 5 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'aria_attribute_tag_count' => (int) $parts[1],
- 'aria_attribute_count' => (int) $parts[2],
- 'unique_aria_attribute_name_count' => (int) $parts[3],
- 'aria_attribute_value_bytes' => (int) $parts[4],
- );
- }
-
- /**
- * Summarizes class inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with class inventory counts.
- */
- private function summarize_class_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_class_inventory' );
-
- $summary = $processor->summarize_class_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 5 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'class_attribute_count' => (int) $parts[1],
- 'class_name_count' => (int) $parts[2],
- 'unique_class_name_count' => (int) $parts[3],
- 'class_value_bytes' => (int) $parts[4],
- );
- }
-
- /**
- * Summarizes resource inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with resource inventory counts.
- */
- private function summarize_resource_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_resource_inventory' );
-
- $summary = $processor->summarize_resource_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 5 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'resource_tag_count' => (int) $parts[1],
- 'resource_attribute_count' => (int) $parts[2],
- 'unique_resource_tag_name_count' => (int) $parts[3],
- 'resource_value_bytes' => (int) $parts[4],
- );
- }
-
- /**
- * Summarizes image inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with image inventory counts.
- */
- private function summarize_image_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_image_inventory' );
-
- $summary = $processor->summarize_image_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 8 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'image_count' => (int) $parts[1],
- 'src_count' => (int) $parts[2],
- 'alt_count' => (int) $parts[3],
- 'empty_alt_count' => (int) $parts[4],
- 'dimension_count' => (int) $parts[5],
- 'src_value_bytes' => (int) $parts[6],
- 'alt_value_bytes' => (int) $parts[7],
- );
- }
-
- /**
- * Summarizes script inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with script inventory counts.
- */
- private function summarize_script_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_script_inventory' );
-
- $summary = $processor->summarize_script_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 8 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'script_count' => (int) $parts[1],
- 'src_count' => (int) $parts[2],
- 'module_count' => (int) $parts[3],
- 'async_count' => (int) $parts[4],
- 'defer_count' => (int) $parts[5],
- 'inline_script_bytes' => (int) $parts[6],
- 'src_value_bytes' => (int) $parts[7],
- );
- }
-
- /**
- * Summarizes form inventory for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param bool $closers Whether to include closing tags.
- * @return array Summary with form inventory counts.
- */
- private function summarize_form_inventory( $processor, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_form_inventory' );
-
- $summary = $processor->summarize_form_inventory( $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 6 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'form_count' => (int) $parts[1],
- 'control_count' => (int) $parts[2],
- 'named_control_count' => (int) $parts[3],
- 'unique_control_name_count' => (int) $parts[4],
- 'control_name_value_bytes' => (int) $parts[5],
- );
- }
-
- /**
- * Summarizes matching tag attributes for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $tag_name Tag name to match.
- * @param array $attribute_names Attribute names to read.
- * @param bool $closers Whether to include closing tags in the tag count.
- * @return array Summary with `tag_count`, `attribute_count`, and `attribute_value_bytes`.
- */
- private function summarize_matching_tag_attributes( $processor, $implementation, $tag_name, $attribute_names, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'summarize_matching_tag_attributes' );
-
- if ( 'native-tag-processor' === $implementation ) {
- $summary = $processor->summarize_matching_tag_attributes( $tag_name, implode( "\x1f", $attribute_names ), $closers );
- } else {
- $summary = $processor->summarize_matching_tag_attributes( $tag_name, $attribute_names, $closers );
- }
-
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 3 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'attribute_count' => (int) $parts[1],
- 'attribute_value_bytes' => (int) $parts[2],
- );
- }
-
- /**
- * Removes prefixed attributes for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $prefix Attribute-name prefix.
- * @param bool $closers Whether to include closing tags in the tag count.
- * @return array Summary with `tag_count`, `removed_count`, and `html`.
- */
- private function remove_attributes_with_prefix_from_document( $processor, $prefix, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'remove_attributes_with_prefix_from_document' );
-
- $summary = $processor->remove_attributes_with_prefix_from_document( $prefix, $closers );
- if ( is_array( $summary ) ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 3 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'removed_count' => (int) $parts[1],
- 'html' => $parts[2],
- );
- }
-
- /**
- * Reads a chunked tag-prefix summary for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $prefix Attribute-name prefix.
- * @param int $max_tags Maximum number of tag rows.
- * @param bool $closers Whether to include closing tags.
- * @return array[] Tag summary rows.
- */
- private function next_tag_prefix_summary_batch( $processor, $implementation, $prefix, $max_tags, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'next_tag_prefix_summary_batch' );
-
- $batch = $processor->next_tag_prefix_summary_batch( $prefix, $max_tags, $closers );
- if ( 'native-tag-processor' !== $implementation ) {
- return $batch;
- }
-
- if ( ! is_string( $batch ) || '' === $batch ) {
- return array();
- }
-
- $rows = array();
- foreach ( explode( "\x1e", $batch ) as $metadata ) {
- $parts = explode( "\x1f", $metadata, 3 );
- $this->assertCount( 3, $parts, 'Expected compact HTML tag summary row.' );
-
- $rows[] = array(
- 'tag_name' => $parts[0],
- 'is_tag_closer' => '1' === $parts[1],
- 'attribute_count' => (int) $parts[2],
- );
- }
-
- return $rows;
- }
-
- /**
- * Reads a chunked tag-prefix count summary for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $prefix Attribute-name prefix.
- * @param int $max_tags Maximum number of tags to consume.
- * @param bool $closers Whether to include closing tags.
- * @return array|null Summary with `tag_count` and `attribute_count`, or null when exhausted.
- */
- private function next_tag_prefix_count_batch( $processor, $prefix, $max_tags, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'next_tag_prefix_count_compact_batch' );
-
- $summary = $processor->next_tag_prefix_count_compact_batch( $prefix, $max_tags, $closers );
- if ( ! is_string( $summary ) || '' === $summary ) {
- return null;
- }
-
- $parts = explode( "\x1f", $summary, 2 );
- $this->assertCount( 2, $parts, 'Expected compact HTML tag prefix count batch.' );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'attribute_count' => (int) $parts[1],
- );
- }
-
- /**
- * Reads a chunked tag summary for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param int $max_tags Maximum number of tag rows.
- * @param bool $closers Whether to include closing tags.
- * @return array[] Tag summary rows.
- */
- private function next_tag_summary_batch( $processor, $implementation, $max_tags, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'next_tag_summary_batch' );
-
- return $processor->next_tag_summary_batch( $max_tags, $closers );
- }
-
- /**
- * Reads a chunked matching-tag summary for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $tag_name Tag name to match.
- * @param int $max_tags Maximum number of tag rows.
- * @param bool $closers Whether to include closing tags.
- * @return array[] Tag summary rows.
- */
- private function next_matching_tag_summary_batch( $processor, $implementation, $tag_name, $max_tags, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'next_matching_tag_summary_batch' );
-
- return $processor->next_matching_tag_summary_batch( $tag_name, $max_tags, $closers );
- }
-
- /**
- * Reads a chunked matching-tag attribute summary for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $tag_name Tag name to match.
- * @param string $attribute_name Attribute name to read.
- * @param int $max_tags Maximum number of tag rows.
- * @param bool $closers Whether to include closing tags.
- * @return array[] Tag summary rows.
- */
- private function next_matching_tag_attribute_summary_batch( $processor, $implementation, $tag_name, $attribute_name, $max_tags, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'next_matching_tag_attribute_summary_batch' );
-
- return $processor->next_matching_tag_attribute_summary_batch( $tag_name, $attribute_name, $max_tags, $closers );
- }
-
- /**
- * Reads a chunked matching-tag multi-attribute summary for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $tag_name Tag name to match.
- * @param array $attribute_names Attribute names to read.
- * @param int $max_tags Maximum number of tag rows.
- * @param bool $closers Whether to include closing tags.
- * @return array[] Tag summary rows.
- */
- private function next_matching_tag_attributes_summary_batch( $processor, $implementation, $tag_name, $attribute_names, $max_tags, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'next_matching_tag_attributes_summary_batch' );
-
- return $processor->next_matching_tag_attributes_summary_batch( $tag_name, $attribute_names, $max_tags, $closers );
- }
-
- /**
- * Reads a compact chunked matching-tag multi-attribute summary.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $tag_name Tag name to match.
- * @param array $attribute_names Attribute names to read.
- * @param int $max_tags Maximum number of tag rows.
- * @param bool $closers Whether to include closing tags.
- * @return string|null Compact tag summary batch, or null when exhausted.
- */
- private function next_matching_tag_attributes_compact_summary_batch( $processor, $implementation, $tag_name, $attribute_names, $max_tags, $closers ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'next_matching_tag_attributes_compact_summary_batch' );
-
- if ( 'native-tag-processor' === $implementation ) {
- return $processor->next_matching_tag_attributes_compact_summary_batch( $tag_name, implode( "\x1f", $attribute_names ), $max_tags, $closers );
- }
-
- return $processor->next_matching_tag_attributes_compact_summary_batch( $tag_name, $attribute_names, $max_tags, $closers );
- }
-
- /**
- * Reads a chunked HTML token summary for either public or native processors.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param int $max_tokens Maximum number of token rows.
- * @return array[] Token summary rows.
- */
- private function next_html_token_summary_batch( $processor, $implementation, $max_tokens ) {
- $this->skip_if_html_method_is_unavailable( $processor, 'next_token_summary_batch' );
-
- return $processor->next_token_summary_batch( $max_tokens );
- }
-
- /**
- * Skips native implementation cases when the extension is unavailable.
- *
- * @param string $implementation Implementation identifier.
- * @param string $class_name Native class name.
- */
- private function skip_if_native_is_unavailable( $implementation, $class_name ) {
- if ( 0 === strpos( $implementation, 'native-' ) && ! class_exists( $class_name, false ) ) {
- $this->markTestSkipped( $class_name . ' is not registered; load the native API extension to run this case.' );
- }
- }
-}
diff --git a/components/HTML/class-wp-html-native-processor-wrapper.php b/components/HTML/class-wp-html-native-processor-wrapper.php
deleted file mode 100644
index 55cbf3761..000000000
--- a/components/HTML/class-wp-html-native-processor-wrapper.php
+++ /dev/null
@@ -1,16 +0,0 @@
-libxml2.
-
-When the native API extension is loaded, XMLProcessor can use a
-native delegate by default while preserving PHP fallback behavior. Define
-WP_NATIVE_APIS_DISABLE_DEFAULTS before loading the component to
-force the pure PHP fallback.
+A streaming, namespace-aware XML processor in pure PHP. Read huge feeds, WXR exports, ePub manifests, and Office Open XML parts incrementally, then emit edited XML without depending on libxml2.
## Why this exists
-SimpleXMLElement and DOMDocument both need libxml2 and both build a complete in-memory tree. XMLProcessor walks the document forward as a cursor, keeps modifications in a side buffer, and emits the full updated XML with get_updated_xml() only when you ask for it.
+SimpleXMLElement and DOMDocument both need libxml2 and both build a complete in-memory tree. XMLProcessor walks the document forward as a cursor, keeps modifications in a side buffer, and emits the full updated XML with get_updated_xml() only when you ask for it. Reading stays incremental; materializing the updated XML still allocates the resulting document.
This design came from WordPress-scale documents such as WXR exports. A migration may only need to rewrite wp:attachment_url values or bump a feed attribute, so the processor optimizes for targeted cursor edits instead of a full validating XML stack.
@@ -118,7 +113,7 @@ wp/status: publish
## Rewrite URLs across an entire WXR export
-Large WXR exports can hold many URLs in <link>, <guid>, and post content. Streaming the file lets you rewrite large exports without loading the whole XML document into memory.
+Large WXR exports can hold many URLs in <link>, <guid>, and post content. The cursor can find and rewrite matching text nodes incrementally; calling get_updated_xml() returns the complete rewritten document.
- Text & More
' );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- $this->assertTrue( $processor->next_tag( '', 'item' ) );
- $this->assertSame( 'Text & More', $processor->get_modifiable_text() );
- $this->assertNull( $this->get_native_delegate( $processor ) );
- }
-
- /**
- * Verifies public native-backed XML processors preserve bookmark lifecycle behavior.
- */
- public function test_public_xml_bookmarks_with_native_defaults() {
- if ( ! class_exists( 'WordPress\\XML\\NativeXMLProcessor', false ) ) {
- $this->markTestSkipped( 'WordPress\\XML\\NativeXMLProcessor is not registered; load the native API extension to run this case.' );
- }
-
- $processor = XMLProcessor::create_from_string( ' ' );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
-
- $this->assertTrue( $processor->next_tag( 'item' ) );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( 'one', $processor->get_attribute( '', 'id' ) );
- $this->assertTrue( $processor->set_bookmark( 'saved-item' ) );
- $this->assertTrue( $processor->has_bookmark( 'saved-item' ) );
-
- $this->assertTrue( $processor->next_tag( 'item' ) );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( 'two', $processor->get_attribute( '', 'id' ) );
-
- $this->assertTrue( $processor->seek( 'saved-item' ) );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( 'one', $processor->get_attribute( '', 'id' ) );
- $this->assertSame(
- array( array( '', 'root' ), array( '', 'item' ) ),
- $processor->get_breadcrumbs()
- );
-
- $this->assertTrue( $processor->release_bookmark( 'saved-item' ) );
- $this->assertFalse( $processor->has_bookmark( 'saved-item' ) );
- }
-
- /**
- * Verifies native-backed XML bookmark seeks preserve PHP mutation handoff state.
- */
- public function test_public_xml_bookmark_seek_then_mutation_with_native_defaults() {
- if ( ! class_exists( 'WordPress\\XML\\NativeXMLProcessor', false ) ) {
- $this->markTestSkipped( 'WordPress\\XML\\NativeXMLProcessor is not registered; load the native API extension to run this case.' );
- }
-
- $processor = XMLProcessor::create_from_string( '- One
Two ' );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
-
- $this->assertTrue( $processor->next_tag( '', 'item' ) );
- $this->assertTrue( $processor->set_bookmark( 'first-item' ) );
- $this->assertTrue( $processor->next_tag( 'urn:x', 'item' ) );
- $this->assertTrue( $processor->seek( 'first-item' ) );
- $this->assertSame( '1', $processor->get_attribute( '', 'id' ) );
- $this->assertNull( $processor->get_attribute( 'urn:x', 'id' ) );
-
- $this->assertTrue( $processor->set_attribute( '', 'data-two', 'b' ) );
- $this->assertTrue( $processor->remove_attribute( '', 'data-one' ) );
- $this->assertSame(
- '- One
Two ',
- $processor->get_updated_xml()
- );
-
- $processor = XMLProcessor::create_from_string( '- One
- Two
' );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- $this->assertTrue( $processor->next_tag( 'item' ) );
- $this->assertTrue( $processor->next_token() );
- $this->assertSame( 'One', $processor->get_modifiable_text() );
- $this->assertTrue( $processor->set_bookmark( 'first-text' ) );
- $this->assertTrue( $processor->next_tag( 'item' ) );
- $this->assertTrue( $processor->seek( 'first-text' ) );
- $this->assertTrue( $processor->set_modifiable_text( 'Changed & ' ) );
- $this->assertSame(
- '- Changed & <ok>
- Two
',
- $processor->get_updated_xml()
- );
-
- $processor = XMLProcessor::create_from_string( '- One
- Two
' );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- $this->assertTrue( $processor->next_tag( 'root' ) );
- $this->assertTrue( $processor->set_bookmark( 'root' ) );
- $this->assertTrue( $processor->next_tag( array( 'breadcrumbs' => array( 'root', 'item' ), 'match_offset' => 2 ) ) );
- $this->assertSame( '2', $processor->get_attribute( '', 'id' ) );
- $this->assertTrue( $processor->set_bookmark( 'second-item' ) );
- $this->assertTrue( $processor->seek( 'root' ) );
- $this->assertTrue( $processor->set_attribute( '', 'class', 'top' ) );
- $this->assertNull( $this->get_native_delegate( $processor ) );
- $this->assertTrue( $processor->seek( 'second-item' ) );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( '2', $processor->get_attribute( '', 'id' ) );
- $this->assertSame(
- '- One
- Two
',
- $processor->get_updated_xml()
- );
- }
-
- /**
- * Verifies public XML inventory summaries complete native-default processors.
- */
- public function test_public_xml_metadata_inventory_summaries_complete_native_default_processors() {
- if ( ! class_exists( 'WordPress\\XML\\NativeXMLProcessor', false ) ) {
- $this->markTestSkipped( 'WordPress\\XML\\NativeXMLProcessor is not registered; load the native API extension to run this case.' );
- }
-
- $methods = array(
- 'summarize_attribute_inventory',
- 'summarize_id_inventory',
- 'summarize_namespace_inventory',
- 'summarize_text_inventory',
- 'summarize_processing_instruction_inventory',
- 'summarize_comment_inventory',
- 'summarize_payload_inventory',
- );
- $xml = 'Text ';
-
- foreach ( $methods as $method ) {
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
-
- $processor->$method();
- $this->assertTrue( $processor->is_finished(), $method . ' should finish a fresh native-default processor.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- $this->assertTrue( $processor->next_tag() );
-
- $processor->$method();
- $this->assertTrue( $processor->is_finished(), $method . ' should finish a partially advanced native-default processor.' );
- }
- }
-
- /**
- * Verifies public XML aggregate summaries complete native-default processors.
- */
- public function test_public_xml_aggregate_summaries_complete_native_default_processors() {
- if ( ! class_exists( 'WordPress\\XML\\NativeXMLProcessor', false ) ) {
- $this->markTestSkipped( 'WordPress\\XML\\NativeXMLProcessor is not registered; load the native API extension to run this case.' );
- }
-
- $cases = array(
- array( 'summarize_attribute_names_with_prefix', array( null, 'data-' ) ),
- array( 'summarize_token_stream', array( 'id' ) ),
- array( 'summarize_matching_tag_stream', array( 'https://wordpress.org', 'item', 'id' ) ),
- array( 'summarize_matching_tag_attributes_stream', array( 'https://wordpress.org', 'item', array( 'id', 'data-kind' ) ) ),
- array( 'summarize_tag_stream', array( 'id' ) ),
- );
- $xml = 'Text ';
-
- foreach ( $cases as $case ) {
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
-
- call_user_func_array( array( $processor, $case[0] ), $case[1] );
- $this->assertTrue( $processor->is_finished(), $case[0] . ' should finish a fresh native-default processor.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- $this->assertTrue( $processor->next_tag() );
-
- call_user_func_array( array( $processor, $case[0] ), $case[1] );
- $this->assertTrue( $processor->is_finished(), $case[0] . ' should finish a partially advanced native-default processor.' );
- }
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
-
- $summary = $processor->remove_attributes_with_prefix_from_document( null, 'data-' );
- $this->assertSame( 2, $summary['removed_count'] );
- $this->assertTrue( $processor->is_finished(), 'remove_attributes_with_prefix_from_document should finish a fresh native-default processor.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- $this->assertTrue( $processor->next_tag() );
-
- $summary = $processor->remove_attributes_with_prefix_from_document( null, 'data-' );
- $this->assertSame( 2, $summary['removed_count'] );
- $this->assertTrue( $processor->is_finished(), 'remove_attributes_with_prefix_from_document should finish a partially advanced native-default processor.' );
- }
-
- /**
- * Verifies public XML batch scans complete native-default processors.
- */
- public function test_public_xml_batches_complete_native_default_processors() {
- if ( ! class_exists( 'WordPress\\XML\\NativeXMLProcessor', false ) ) {
- $this->markTestSkipped( 'WordPress\\XML\\NativeXMLProcessor is not registered; load the native API extension to run this case.' );
- }
-
- $xml = '- Text
';
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $batch = $processor->next_token_summary_batch( 2 );
- } while ( ! empty( $batch ) );
- $this->assertTrue( $processor->is_finished(), 'next_token_summary_batch should finish after exhaustion.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $batch = $processor->next_tag_summary_batch( 2, 'id' );
- } while ( ! empty( $batch ) );
- $this->assertTrue( $processor->is_finished(), 'next_tag_summary_batch should finish after exhaustion.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $summary = $processor->next_tag_count_batch( 2, 'id' );
- } while ( $summary['token_count'] > 0 );
- $this->assertTrue( $processor->is_finished(), 'next_tag_count_batch should finish after exhaustion.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $batch = $processor->next_matching_tag_summary_batch( 1, '', 'item', 'id' );
- } while ( ! empty( $batch ) );
- $this->assertTrue( $processor->is_finished(), 'next_matching_tag_summary_batch should finish after exhaustion.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $summary = $processor->next_matching_tag_count_batch( 1, '', 'item', 'id' );
- } while ( $summary['token_count'] > 0 );
- $this->assertTrue( $processor->is_finished(), 'next_matching_tag_count_batch should finish after exhaustion.' );
- }
-
- /**
- * Verifies public XML compact batch scans complete native-default processors.
- */
- public function test_public_xml_compact_batches_complete_native_default_processors() {
- if ( ! class_exists( 'WordPress\\XML\\NativeXMLProcessor', false ) ) {
- $this->markTestSkipped( 'WordPress\\XML\\NativeXMLProcessor is not registered; load the native API extension to run this case.' );
- }
-
- $xml = '- Text
';
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $batch = $processor->next_token_compact_summary_batch( 2 );
- } while ( is_string( $batch ) && '' !== $batch );
- $this->assertTrue( $processor->is_finished(), 'next_token_compact_summary_batch should finish after exhaustion.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $batch = $processor->next_tag_compact_summary_batch( 2, 'id' );
- } while ( is_string( $batch ) && '' !== $batch );
- $this->assertTrue( $processor->is_finished(), 'next_tag_compact_summary_batch should finish after exhaustion.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $summary = $processor->next_tag_count_compact_batch( 2, 'id' );
- $parts = explode( "\x1f", is_string( $summary ) ? $summary : '' );
- } while ( isset( $parts[0] ) && (int) $parts[0] > 0 );
- $this->assertTrue( $processor->is_finished(), 'next_tag_count_compact_batch should finish after exhaustion.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $batch = $processor->next_matching_tag_compact_summary_batch( 1, '', 'item', 'id' );
- } while ( is_string( $batch ) && '' !== $batch );
- $this->assertTrue( $processor->is_finished(), 'next_matching_tag_compact_summary_batch should finish after exhaustion.' );
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->assertSame( 'WordPress\\XML\\NativeXMLProcessor', get_class( $this->get_native_delegate( $processor ) ) );
- do {
- $summary = $processor->next_matching_tag_count_compact_batch( 1, '', 'item', 'id' );
- $parts = explode( "\x1f", is_string( $summary ) ? $summary : '' );
- } while ( isset( $parts[0] ) && (int) $parts[0] > 0 );
- $this->assertTrue( $processor->is_finished(), 'next_matching_tag_count_compact_batch should finish after exhaustion.' );
- }
-
- /**
- * Verifies complete-input processors reject appended bytes.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_rejects_append_bytes_for_complete_input( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertFalse( $processor->append_bytes( ' ' ) );
- $this->assertFalse( $processor->is_expecting_more_input() );
- $this->assertFalse( $processor->is_paused_at_incomplete_input() );
- }
-
- /**
- * Verifies streaming processors can resume after incomplete input.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_streaming_appends_after_incomplete_input( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_streaming_processor( $implementation, 'assertFalse( $processor->next_token(), 'Expected incomplete opening tag to pause token scanning.' );
- $this->assertNull( $processor->get_last_error() );
- $this->assertTrue( $processor->is_paused_at_incomplete_input() );
- $this->assertTrue( $processor->is_expecting_more_input() );
- $this->assertTrue( $processor->append_bytes( '> ' ) );
-
- $this->assertTrue( $processor->next_token(), 'Expected root tag after appending bytes.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertTrue( $processor->next_token(), 'Expected item tag after appending bytes.' );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( '1', $this->get_xml_attribute( $processor, $implementation, 'id' ) );
- $this->assertTrue( $processor->next_token(), 'Expected root closer after appending bytes.' );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertTrue( $processor->is_tag_closer() );
- }
-
- /**
- * Verifies streaming processors resume after an incomplete token following prior tokens.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_streaming_appends_after_prior_token_incomplete_input( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_streaming_processor( $implementation, '- assertTrue( $processor->next_token(), 'Expected root tag before incomplete child.' );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertFalse( $processor->next_token(), 'Expected incomplete child tag to pause token scanning.' );
- $this->assertNull( $processor->get_last_error() );
- $this->assertTrue( $processor->is_paused_at_incomplete_input() );
- $this->assertTrue( $processor->is_expecting_more_input() );
- $this->assertTrue( $processor->append_bytes( ' id="1" />
' ) );
-
- $this->assertTrue( $processor->next_token(), 'Expected child tag after appending bytes.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( '1', $this->get_xml_attribute( $processor, $implementation, 'id' ) );
- $this->assertTrue( $processor->next_token(), 'Expected root closer after appended child.' );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertTrue( $processor->is_tag_closer() );
- }
-
- /**
- * Verifies streaming cursors preserve context for siblings but not parent closers.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_streaming_cursor_preserves_sibling_context( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = 'Text ';
- $processor = $this->create_xml_streaming_processor( $implementation, $xml );
-
- $this->assertTrue( $processor->next_tag(), 'Expected root tag before cursor capture.' );
- $this->assertTrue( $processor->next_tag(), 'Expected item tag before cursor capture.' );
- $this->assertSame( 'item', $processor->get_tag_local_name() );
- $this->assertSame( 'https://wordpress.org', $processor->get_tag_namespace() );
-
- $offset = $processor->get_token_byte_offset_in_the_input_stream();
- $cursor = $processor->get_reentrancy_cursor();
- $resumed = $this->create_xml_streaming_processor_from_cursor( $implementation, substr( $xml, $offset ), $cursor );
-
- $this->assertTrue( $resumed->next_tag(), 'Expected resumed processor to expose the item tag.' );
- $this->assertSame( 'item', $resumed->get_tag_local_name() );
- $this->assertSame( 'https://wordpress.org', $resumed->get_tag_namespace() );
- $this->assertSame( '7', $this->get_xml_attribute( $resumed, $implementation, 'id' ) );
- $this->assertTrue( $resumed->next_token(), 'Expected resumed processor to expose item text.' );
- $this->assertSame( 'Text', $resumed->get_modifiable_text() );
- $this->assertTrue( $resumed->next_token(), 'Expected resumed processor to expose item closer.' );
- $this->assertSame( 'item', $resumed->get_tag_local_name() );
- $this->assertTrue( $resumed->is_tag_closer() );
- $this->assertTrue( $resumed->next_token(), 'Expected resumed processor to expose sibling tail tag.' );
- $this->assertSame( 'tail', $resumed->get_tag_local_name() );
- $this->assertSame( 'https://wordpress.org', $resumed->get_tag_namespace() );
- $this->assertTrue( $resumed->is_empty_element() );
- $this->assertFalse( $resumed->next_token(), 'Expected resumed processor to reject the pre-cursor parent closer.' );
- $this->assertNotNull( $resumed->get_last_error() );
- }
-
- /**
- * Verifies streaming batch scans pause at incomplete input and resume after append.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_streaming_batches_pause_at_incomplete_input( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $cases = array(
- 'token_compact',
- 'tag_compact',
- 'tag_count',
- 'matching_tag_compact',
- 'matching_tag_count',
- );
-
- foreach ( $cases as $case ) {
- $processor = $this->create_xml_streaming_processor( $implementation, '- run_xml_streaming_batch_case( $processor, $implementation, $case );
-
- if ( 'matching_tag_compact' === $case ) {
- $this->assertNull( $result, 'Expected no matching compact row before the incomplete child is complete.' );
- } else {
- $this->assertNotEmpty( $result, 'Expected batch case ' . $case . ' to expose prior complete tokens.' );
- }
-
- $this->assertNull( $processor->get_last_error(), 'Expected batch case ' . $case . ' not to report an error before input is finished.' );
- $this->assertTrue( $processor->is_paused_at_incomplete_input(), 'Expected batch case ' . $case . ' to pause at incomplete input.' );
- $this->assertTrue( $processor->is_expecting_more_input(), 'Expected batch case ' . $case . ' to keep expecting input.' );
- $this->assertTrue( $processor->append_bytes( ' id="1" />
' ), 'Expected batch case ' . $case . ' to accept appended bytes.' );
- $this->assertTrue( $processor->next_token(), 'Expected batch case ' . $case . ' to resume at the incomplete child tag.' );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( '1', $this->get_xml_attribute( $processor, $implementation, 'id' ) );
- }
- }
-
- /**
- * Verifies finishing incomplete streaming input reports a syntax error on the next scan.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_streaming_input_finished_reports_incomplete_input_error( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_streaming_processor( $implementation, '- assertTrue( $processor->next_token(), 'Expected root tag before incomplete child.' );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertFalse( $processor->next_token(), 'Expected incomplete child tag to pause token scanning.' );
- $this->assertNull( $processor->get_last_error() );
- $this->assertTrue( $processor->is_paused_at_incomplete_input() );
- $this->assertTrue( $processor->is_expecting_more_input() );
-
- $processor->input_finished();
-
- $this->assertNull( $processor->get_last_error(), 'Expected finished input to defer the syntax error until the next scan.' );
- $this->assertFalse( $processor->is_paused_at_incomplete_input() );
- $this->assertFalse( $processor->is_expecting_more_input() );
- $this->assertFalse( $processor->is_finished() );
- $this->assertFalse( $processor->next_token(), 'Expected finished incomplete input to report a syntax error.' );
- $this->assertNotNull( $processor->get_last_error() );
- $this->assertFalse( $processor->is_finished() );
- }
-
- /**
- * Verifies XML documents without a document element report a syntax error.
- *
- * @dataProvider data_xml_without_document_element
- *
- * @param string $xml XML input.
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_requires_document_element( $xml, $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- while ( $processor->next_token() ) {
- continue;
- }
-
- $this->assertNotNull( $processor->get_last_error() );
- }
-
- /**
- * Verifies leading whitespace outside the document element is skipped.
- *
- * @dataProvider data_xml_with_leading_misc_whitespace
- *
- * @param string $xml XML input.
- * @param array $token_names Expected token names.
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_skips_leading_misc_whitespace( $xml, $token_names, $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- foreach ( $token_names as $token_name ) {
- $this->assertTrue( $processor->next_token() );
- $this->assertSame( $token_name, $processor->get_token_name() );
- }
- }
-
- /**
- * Verifies streaming processors pause and resume for incomplete token kinds.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_streaming_pauses_for_incomplete_token_kinds( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $cases = array(
- array( '
' ),
- array( ' ' ),
- array( ' ' ),
- array( ' ' ),
- array( '<', 'root />' ),
- array( ' ' ),
- array( ' ' ),
- array( ' ' ),
- );
-
- foreach ( $cases as $case ) {
- $processor = $this->create_xml_streaming_processor( $implementation, $case[0] );
- while ( $processor->next_token() ) {
- // Advance to the incomplete token.
- }
-
- $this->assertTrue( $processor->is_paused_at_incomplete_input(), 'Expected incomplete input to pause for: ' . $case[0] );
- $this->assertTrue( $processor->is_expecting_more_input(), 'Expected processor to keep expecting input for: ' . $case[0] );
- $this->assertTrue( $processor->append_bytes( $case[1] ), 'Expected processor to accept appended bytes for: ' . $case[0] );
- $this->assertTrue( $processor->next_token(), 'Expected processor to resume after appended bytes for: ' . $case[0] );
- }
- }
-
- /**
- * Verifies the first native slice matches the PHP XML processor token stream.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_tags_and_attributes( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the root token.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_tag_namespace() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the item tag.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_tag_namespace() );
- $this->assertSame( '7', $this->get_xml_attribute( $processor, $implementation, 'id' ) );
- $this->assertSame( 'post', $this->get_xml_attribute( $processor, $implementation, 'data-kind' ) );
- $this->assertSame(
- array( array( '', 'data-kind' ) ),
- $processor->get_attribute_names_with_prefix( null, 'data-' )
- );
- $this->assertNull( $processor->get_last_error() );
- }
-
- /**
- * Verifies attribute-prefix reads preserve XML input order.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_preserves_attribute_prefix_order( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the content tag.' );
- $this->assertSame(
- array(
- array( 'http://wordpress.org/export/1.2/', 'data-foo' ),
- array( 'http://wordpress.org/export/1.2/', 'data-bar' ),
- ),
- $processor->get_attribute_names_with_prefix( 'http://wordpress.org/export/1.2/', 'data-' )
- );
- $this->assertSame(
- array( array( '', 'data-foo' ) ),
- $processor->get_attribute_names_with_prefix( null, 'data-' )
- );
- }
-
- /**
- * Verifies document-level attribute-prefix summaries match repeated scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_attribute_prefixes( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 3,
- 'attribute_count' => 2,
- ),
- $this->summarize_xml_attribute_names_with_prefix( $processor, $implementation, null, 'data-' )
- );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the root tag before summarizing remaining tags.' );
- $this->assertSame(
- array(
- 'tag_count' => 2,
- 'attribute_count' => 1,
- ),
- $this->summarize_xml_attribute_names_with_prefix( $processor, $implementation, 'http://wordpress.org/export/1.2/', 'data-' )
- );
- }
-
- /**
- * Verifies document-level token stream summaries match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_token_streams( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
-
- $this->assertSame(
- array(
- 'token_count' => 8,
- 'tag_count' => 3,
- 'attribute_count' => 2,
- ),
- $this->summarize_xml_token_stream( $processor, $implementation, 'id' )
- );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the XML declaration before summarizing remaining tokens.' );
- $this->assertSame(
- array(
- 'token_count' => 7,
- 'tag_count' => 3,
- 'attribute_count' => 2,
- ),
- $this->summarize_xml_token_stream( $processor, $implementation, 'id' )
- );
- }
-
- /**
- * Verifies document-level tag stream summaries match repeated tag scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_tag_streams( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
-
- $this->assertSame(
- array(
- 'tag_count' => 3,
- 'attribute_count' => 2,
- ),
- $this->summarize_xml_tag_stream( $processor, $implementation, 'id' )
- );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the root tag before summarizing remaining tags.' );
- $this->assertSame(
- array(
- 'tag_count' => 2,
- 'attribute_count' => 1,
- ),
- $this->summarize_xml_tag_stream( $processor, $implementation, 'id' )
- );
- }
-
- /**
- * Verifies incremental tag count batches match repeated tag scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_counts_tag_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
-
- $this->assertSame(
- array(
- 'token_count' => 3,
- 'tag_count' => 2,
- 'attribute_count' => 2,
- ),
- $this->next_xml_tag_count_batch( $processor, $implementation, 2, 'id' )
- );
- $this->assertSame(
- array(
- 'token_count' => 5,
- 'tag_count' => 1,
- 'attribute_count' => 0,
- ),
- $this->next_xml_tag_count_batch( $processor, $implementation, 2, 'id' )
- );
- $this->assertSame(
- array(
- 'token_count' => 0,
- 'tag_count' => 0,
- 'attribute_count' => 0,
- ),
- $this->next_xml_tag_count_batch( $processor, $implementation, 2, 'id' )
- );
- }
-
- /**
- * Verifies document-level prefixed attribute removals match repeated scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_removes_attribute_prefixes_from_document( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = ' ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'tag_count' => 3,
- 'removed_count' => 3,
- 'xml' => ' ',
- ),
- $this->remove_xml_attribute_names_with_prefix_from_document( $processor, $implementation, null, 'data-' )
- );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertSame(
- array(
- 'tag_count' => 3,
- 'removed_count' => 1,
- 'xml' => ' ',
- ),
- $this->remove_xml_attribute_names_with_prefix_from_document( $processor, $implementation, 'http://wordpress.org/export/1.2/', 'data-' )
- );
- }
-
- /**
- * Verifies empty-element markers are exposed consistently.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reports_empty_elements( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the root token.' );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertFalse( $processor->is_empty_element() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the empty element token.' );
- $this->assertSame( 'empty', $processor->get_token_name() );
- $this->assertTrue( $processor->is_empty_element() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the non-empty parent token.' );
- $this->assertSame( 'parent', $processor->get_token_name() );
- $this->assertFalse( $processor->is_empty_element() );
- }
-
- /**
- * Verifies XML character references are decoded in text and attributes.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_decodes_character_references( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '& & < > " 'CD &unknown; '
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the root token.' );
- $this->assertSame( '& & < > " \'AB &unknown;', $this->get_xml_attribute( $processor, $implementation, 'a' ) );
-
- $this->assertTrue( $processor->next_token(), 'Expected the text token.' );
- $this->assertSame( '& & < > " \'CD &unknown;', $processor->get_modifiable_text() );
- $this->assertNull( $processor->get_last_error() );
- }
-
- /**
- * Verifies malformed documents are observable through the shared error API.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reports_syntax_errors( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- while ( $processor->next_token() ) {
- continue;
- }
-
- $this->assertNotNull( $processor->get_last_error() );
- }
-
- /**
- * Verifies malformed closing tags stop scanning after prior valid tokens.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_rejects_attributes_in_closing_tags( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'Test '
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the opening tag before the invalid closer.' );
- $this->assertSame( 'content', $processor->get_token_name() );
- $this->assertTrue( $processor->next_token(), 'Expected the text token before the invalid closer.' );
- $this->assertSame( 'Test', $processor->get_modifiable_text() );
- $this->assertFalse( $processor->next_token(), 'Expected the invalid closing tag to stop token scanning.' );
- $this->assertNotNull( $processor->get_last_error() );
- }
-
- /**
- * Verifies malformed attributes reject the current token.
- *
- * @dataProvider data_malformed_attribute_xml_processor_implementations
- *
- * @param string $xml XML input.
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_rejects_malformed_attributes( $xml, $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertFalse( $processor->next_tag(), 'Expected malformed attributes to stop tag scanning.' );
- $this->assertNotNull( $processor->get_last_error() );
- $this->assertNotNull( $processor->get_exception() );
- }
-
- /**
- * Verifies unsupported processing instructions are observable as errors.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reports_processing_instruction_errors( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- while ( $processor->next_token() ) {
- continue;
- }
-
- $this->assertNotNull( $processor->get_last_error() );
- }
-
- /**
- * Verifies namespace-local tag reads and namespaced attributes stay aligned.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_namespace_qualified_tags_and_attributes( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the root tag.' );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertSame( 'root', $processor->get_tag_local_name() );
- $this->assertSame( 'root', $processor->get_tag_namespace_and_local_name() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the namespaced item tag.' );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( 'item', $processor->get_tag_local_name() );
- $this->assertSame( 'https://wordpress.org', $processor->get_tag_namespace() );
- $this->assertSame( '{https://wordpress.org}item', $processor->get_tag_namespace_and_local_name() );
- $this->assertSame( '7', $this->get_xml_attribute( $processor, $implementation, 'id', 'https://wordpress.org' ) );
- $this->assertSame( 'yes', $this->get_xml_attribute( $processor, $implementation, 'plain' ) );
- }
-
- /**
- * Verifies breadcrumbs and current depth stay aligned while skipping closing tags.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reports_breadcrumbs_and_current_depth( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertSame( 0, $processor->get_current_depth() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the root tag.' );
- $this->assertSame( 1, $processor->get_current_depth() );
- $this->assertSame(
- array( array( '', 'root' ) ),
- $processor->get_breadcrumbs()
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the namespaced text tag.' );
- $this->assertSame( 2, $processor->get_current_depth() );
- $this->assertSame(
- array( array( '', 'root' ), array( 'w.org', 'text' ) ),
- $processor->get_breadcrumbs()
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected the post tag.' );
- $this->assertSame( 3, $processor->get_current_depth() );
- $this->assertSame(
- array( array( '', 'root' ), array( 'w.org', 'text' ), array( '', 'post' ) ),
- $processor->get_breadcrumbs()
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip closing tags and find image.' );
- $this->assertSame( 'image', $processor->get_token_name() );
- $this->assertSame( 2, $processor->get_current_depth() );
- $this->assertSame(
- array( array( '', 'root' ), array( '', 'image' ) ),
- $processor->get_breadcrumbs()
- );
- }
-
- /**
- * Verifies text and comment tokens stay aligned with the PHP XML processor.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_text_and_comment_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'Hello World '
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the root tag.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'root', $processor->get_token_name() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the namespaced text tag.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'text', $processor->get_token_name() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the first text token.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( '#text', $processor->get_token_name() );
- $this->assertSame( 'Hello', $processor->get_modifiable_text() );
- $this->assertSame( 2, $processor->get_current_depth() );
- $this->assertSame(
- array( array( '', 'root' ), array( 'w.org', 'text' ) ),
- $processor->get_breadcrumbs()
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the comment token.' );
- $this->assertSame( '#comment', $processor->get_token_type() );
- $this->assertSame( '#comment', $processor->get_token_name() );
- $this->assertSame( 'note', $processor->get_modifiable_text() );
- $this->assertSame( 2, $processor->get_current_depth() );
- $this->assertSame(
- array( array( '', 'root' ), array( 'w.org', 'text' ) ),
- $processor->get_breadcrumbs()
- );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip text and find the post tag.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'post', $processor->get_token_name() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the second text token.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( 'World', $processor->get_modifiable_text() );
- }
-
- /**
- * Verifies opening tag modifiable text stays aligned with the PHP XML processor.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_opening_tag_modifiable_text( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text & More
'
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the root tag.' );
- $this->assertSame( 'root', $processor->get_token_name() );
- $this->assertSame( '- Text & More
', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the item tag.' );
- $this->assertSame( 'item', $processor->get_token_name() );
- $this->assertSame( 'Text & More', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the text token.' );
- $this->assertSame( '#text', $processor->get_token_name() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the item closer.' );
- $this->assertSame( 'item', $processor->get_token_name() );
-
- $processor = $this->create_xml_processor( $implementation, ' ' );
- $this->assertTrue( $processor->next_token(), 'Expected the root tag before the empty element.' );
- $this->assertTrue( $processor->next_token(), 'Expected the empty element.' );
- $this->assertSame( 'empty', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_modifiable_text() );
- }
-
- /**
- * Verifies CDATA sections stay aligned with the PHP XML processor.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_cdata_section_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'before&c]]>after '
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the root tag.' );
- $this->assertTrue( $processor->next_token(), 'Expected the namespaced text tag.' );
-
- $this->assertTrue( $processor->next_token(), 'Expected the text token before CDATA.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( 'before', $processor->get_modifiable_text() );
-
- $this->assertTrue( $processor->next_token(), 'Expected the CDATA token.' );
- $this->assertSame( '#cdata-section', $processor->get_token_type() );
- $this->assertSame( '#cdata-section', $processor->get_token_name() );
- $this->assertSame( '&c', $processor->get_modifiable_text() );
- $this->assertSame( 2, $processor->get_current_depth() );
- $this->assertSame(
- array( array( '', 'root' ), array( 'w.org', 'text' ) ),
- $processor->get_breadcrumbs()
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the text token after CDATA.' );
- $this->assertSame( '#text', $processor->get_token_type() );
- $this->assertSame( 'after', $processor->get_modifiable_text() );
- }
-
- /**
- * Verifies modifiable text updates stay aligned with the PHP XML processor.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_updates_modifiable_text_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'One '
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the root tag.' );
- $this->assertFalse( $processor->set_modifiable_text( 'nope' ), 'Expected tag text mutation to be rejected.' );
-
- $this->assertTrue( $processor->next_token(), 'Expected the text token.' );
- $this->assertTrue( $processor->set_modifiable_text( 'Two & ' ), 'Expected text mutation to succeed.' );
-
- $this->assertTrue( $processor->next_token(), 'Expected the comment token.' );
- $this->assertTrue( $processor->set_modifiable_text( 'comment & ' ), 'Expected comment mutation to succeed.' );
-
- $this->assertTrue( $processor->next_token(), 'Expected the CDATA token.' );
- $this->assertTrue( $processor->set_modifiable_text( 'inside ]]> cdata' ), 'Expected CDATA mutation to succeed.' );
-
- $this->assertSame(
- 'Two & <three> ',
- $processor->get_updated_xml()
- );
- $this->assertTrue( $processor->next_tag(), 'Expected processor to continue after text mutations.' );
- $this->assertSame( 'tail', $processor->get_token_name() );
- }
-
- /**
- * Verifies prolog declaration tokens stay aligned with the PHP XML processor.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_xml_declaration_and_doctype_tokens( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- ' '
- );
-
- $this->assertTrue( $processor->next_token(), 'Expected the XML declaration token.' );
- $this->assertSame( '#xml-declaration', $processor->get_token_type() );
- $this->assertSame( '#xml-declaration', $processor->get_token_name() );
- $this->assertSame( 'xml version="1.0" encoding="UTF-8"', $processor->get_modifiable_text() );
- $this->assertSame( 0, $processor->get_current_depth() );
- $this->assertSame( array(), $processor->get_breadcrumbs() );
- $this->assertSame( '1.0', $this->get_xml_attribute( $processor, $implementation, 'version' ) );
- $this->assertSame( 'UTF-8', $this->get_xml_attribute( $processor, $implementation, 'encoding' ) );
-
- $this->assertTrue( $processor->next_token(), 'Expected the DOCTYPE token.' );
- $this->assertSame( '#doctype', $processor->get_token_type() );
- $this->assertSame( '#doctype', $processor->get_token_name() );
- $this->assertSame( '', $processor->get_modifiable_text() );
- $this->assertSame( 0, $processor->get_current_depth() );
- $this->assertSame( array(), $processor->get_breadcrumbs() );
-
- $this->assertTrue( $processor->next_tag(), 'Expected next_tag() to skip prolog tokens and find the root tag.' );
- $this->assertSame( '#tag', $processor->get_token_type() );
- $this->assertSame( 'root', $processor->get_token_name() );
- }
-
- /**
- * Verifies chunked token summaries stay aligned with token-by-token reads.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_token_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
- $rows = array();
-
- do {
- $batch = $this->next_xml_token_summary_batch( $processor, $implementation, 2 );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertCount( 7, $rows );
- $this->assertSame(
- array( '#xml-declaration', '#tag', '#tag', '#text', '#tag', '#tag', '#tag' ),
- array_column( $rows, 'token_type' )
- );
- $this->assertSame(
- array( null, 'root', '7', null, null, null, null ),
- array_column( $rows, 'id' )
- );
- $this->assertSame( 'root', $rows[1]['tag_local_name'] );
- $this->assertSame( 1, $rows[1]['current_depth'] );
- $this->assertSame( 'item', $rows[2]['tag_local_name'] );
- $this->assertSame( 2, $rows[2]['current_depth'] );
- $this->assertSame( true, $rows[4]['is_tag_closer'] );
- $this->assertSame( true, $rows[5]['is_empty_element'] );
- $this->assertSame( 0, $rows[6]['current_depth'] );
- }
-
- /**
- * Verifies document-level inventory summaries match token-by-token reads.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_document_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
-
- $this->assertSame(
- array(
- 'token_count' => 11,
- 'tag_count' => 4,
- 'closing_tag_count' => 3,
- 'text_token_count' => 1,
- 'comment_count' => 1,
- 'cdata_count' => 1,
- 'max_depth' => 2,
- 'empty_element_count' => 1,
- ),
- $this->summarize_xml_document_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 9,
- 'tag_count' => 3,
- 'closing_tag_count' => 3,
- 'text_token_count' => 1,
- 'comment_count' => 1,
- 'cdata_count' => 1,
- 'max_depth' => 2,
- 'empty_element_count' => 1,
- ),
- $this->summarize_xml_document_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
- }
-
- /**
- * Verifies element-name inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_element_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = ' ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 8,
- 'tag_count' => 5,
- 'closing_tag_count' => 2,
- 'unique_tag_name_count' => 3,
- 'duplicate_tag_name_count' => 2,
- 'namespaced_tag_count' => 3,
- 'empty_element_count' => 3,
- ),
- $this->summarize_xml_element_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 6,
- 'tag_count' => 4,
- 'closing_tag_count' => 2,
- 'unique_tag_name_count' => 2,
- 'duplicate_tag_name_count' => 2,
- 'namespaced_tag_count' => 2,
- 'empty_element_count' => 3,
- ),
- $this->summarize_xml_element_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
- }
-
- /**
- * Verifies document-level depth inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_depth_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = ' ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 10,
- 'tag_count' => 6,
- 'closing_tag_count' => 3,
- 'empty_element_count' => 3,
- 'root_level_tag_count' => 1,
- 'nested_tag_count' => 3,
- 'total_tag_depth' => 15,
- 'max_depth' => 4,
- ),
- $this->summarize_xml_depth_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 8,
- 'tag_count' => 5,
- 'closing_tag_count' => 3,
- 'empty_element_count' => 3,
- 'root_level_tag_count' => 0,
- 'nested_tag_count' => 3,
- 'total_tag_depth' => 14,
- 'max_depth' => 4,
- ),
- $this->summarize_xml_depth_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
- }
-
- /**
- * Verifies leaf and branch element inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_leaf_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = ' ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 10,
- 'tag_count' => 6,
- 'closing_tag_count' => 3,
- 'empty_element_count' => 3,
- 'leaf_element_count' => 3,
- 'branch_element_count' => 3,
- 'max_child_element_count' => 2,
- ),
- $this->summarize_xml_leaf_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 8,
- 'tag_count' => 5,
- 'closing_tag_count' => 3,
- 'empty_element_count' => 3,
- 'leaf_element_count' => 3,
- 'branch_element_count' => 2,
- 'max_child_element_count' => 2,
- ),
- $this->summarize_xml_leaf_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
- }
-
- /**
- * Verifies structural inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_structural_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = ' ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 10,
- 'tag_count' => 6,
- 'closing_tag_count' => 3,
- 'unique_tag_name_count' => 5,
- 'duplicate_tag_name_count' => 1,
- 'namespaced_tag_count' => 4,
- 'empty_element_count' => 3,
- 'root_level_tag_count' => 1,
- 'nested_tag_count' => 3,
- 'total_tag_depth' => 15,
- 'max_depth' => 4,
- 'leaf_element_count' => 3,
- 'branch_element_count' => 3,
- 'max_child_element_count' => 2,
- ),
- $this->summarize_xml_structural_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 8,
- 'tag_count' => 5,
- 'closing_tag_count' => 3,
- 'unique_tag_name_count' => 4,
- 'duplicate_tag_name_count' => 1,
- 'namespaced_tag_count' => 3,
- 'empty_element_count' => 3,
- 'root_level_tag_count' => 0,
- 'nested_tag_count' => 3,
- 'total_tag_depth' => 14,
- 'max_depth' => 4,
- 'leaf_element_count' => 3,
- 'branch_element_count' => 2,
- 'max_child_element_count' => 2,
- ),
- $this->summarize_xml_structural_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
- }
-
- /**
- * Verifies document-level attribute inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_attribute_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = 'Title ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 9,
- 'tag_count' => 4,
- 'attribute_count' => 4,
- 'namespaced_attribute_count' => 1,
- 'tags_with_attributes_count' => 3,
- 'max_attribute_count' => 2,
- ),
- $this->summarize_xml_attribute_inventory( $processor, $implementation )
- );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 7,
- 'tag_count' => 3,
- 'attribute_count' => 3,
- 'namespaced_attribute_count' => 1,
- 'tags_with_attributes_count' => 2,
- 'max_attribute_count' => 2,
- ),
- $this->summarize_xml_attribute_inventory( $processor, $implementation )
- );
- }
-
- /**
- * Verifies document-level ID inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_id_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = 'Title ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 10,
- 'tag_count' => 5,
- 'id_attribute_count' => 4,
- 'unique_id_count' => 3,
- 'duplicate_id_count' => 1,
- 'id_value_bytes' => 11,
- ),
- $this->summarize_xml_id_inventory( $processor, $implementation )
- );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 8,
- 'tag_count' => 4,
- 'id_attribute_count' => 3,
- 'unique_id_count' => 2,
- 'duplicate_id_count' => 1,
- 'id_value_bytes' => 7,
- ),
- $this->summarize_xml_id_inventory( $processor, $implementation )
- );
- }
-
- /**
- * Verifies document-level namespace inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_namespace_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = 'Title ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 9,
- 'tag_count' => 4,
- 'namespaced_tag_count' => 3,
- 'attribute_count' => 5,
- 'namespaced_attribute_count' => 2,
- 'unique_namespace_count' => 2,
- ),
- $this->summarize_xml_namespace_inventory( $processor, $implementation )
- );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 7,
- 'tag_count' => 3,
- 'namespaced_tag_count' => 2,
- 'attribute_count' => 4,
- 'namespaced_attribute_count' => 2,
- 'unique_namespace_count' => 2,
- ),
- $this->summarize_xml_namespace_inventory( $processor, $implementation )
- );
- }
-
- /**
- * Verifies document-level text inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_text_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = ' Alpha - Two
';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 13,
- 'text_token_count' => 3,
- 'cdata_count' => 1,
- 'non_empty_text_count' => 3,
- 'whitespace_text_count' => 1,
- 'total_text_bytes' => 16,
- 'max_text_bytes' => 7,
- ),
- $this->summarize_xml_text_inventory( $processor, $implementation )
- );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 11,
- 'text_token_count' => 3,
- 'cdata_count' => 1,
- 'non_empty_text_count' => 3,
- 'whitespace_text_count' => 1,
- 'total_text_bytes' => 16,
- 'max_text_bytes' => 7,
- ),
- $this->summarize_xml_text_inventory( $processor, $implementation )
- );
- }
-
- /**
- * Verifies document-level processing instruction inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_processing_instruction_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = ' ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 7,
- 'processing_instruction_count' => 3,
- 'xml_declaration_count' => 1,
- 'non_empty_instruction_count' => 4,
- 'total_instruction_bytes' => 64,
- 'max_instruction_bytes' => 27,
- ),
- $this->summarize_xml_processing_instruction_inventory( $processor, $implementation )
- );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 5,
- 'processing_instruction_count' => 3,
- 'xml_declaration_count' => 0,
- 'non_empty_instruction_count' => 3,
- 'total_instruction_bytes' => 47,
- 'max_instruction_bytes' => 27,
- ),
- $this->summarize_xml_processing_instruction_inventory( $processor, $implementation )
- );
- }
-
- /**
- * Verifies document-level comment inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_comment_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = ' ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 12,
- 'comment_count' => 5,
- 'non_empty_comment_count' => 3,
- 'empty_comment_count' => 2,
- 'total_comment_bytes' => 18,
- 'max_comment_bytes' => 7,
- ),
- $this->summarize_xml_comment_inventory( $processor, $implementation )
- );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 10,
- 'comment_count' => 5,
- 'non_empty_comment_count' => 3,
- 'empty_comment_count' => 2,
- 'total_comment_bytes' => 18,
- 'max_comment_bytes' => 7,
- ),
- $this->summarize_xml_comment_inventory( $processor, $implementation )
- );
- }
-
- /**
- * Verifies document-level payload inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_payload_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = 'Alpha ';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 13,
- 'text_token_count' => 2,
- 'cdata_count' => 1,
- 'comment_count' => 1,
- 'processing_instruction_count' => 2,
- 'total_payload_bytes' => 37,
- 'max_payload_bytes' => 11,
- ),
- $this->summarize_xml_payload_inventory( $processor, $implementation )
- );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 11,
- 'text_token_count' => 2,
- 'cdata_count' => 1,
- 'comment_count' => 1,
- 'processing_instruction_count' => 2,
- 'total_payload_bytes' => 37,
- 'max_payload_bytes' => 11,
- ),
- $this->summarize_xml_payload_inventory( $processor, $implementation )
- );
- }
-
- /**
- * Verifies document-level content inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_content_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = '- Title
';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 11,
- 'tag_count' => 3,
- 'attribute_count' => 3,
- 'text_token_count' => 1,
- 'cdata_count' => 1,
- 'comment_count' => 1,
- 'processing_instruction_count' => 2,
- 'total_attribute_value_bytes' => 9,
- 'max_attribute_value_bytes' => 4,
- 'total_payload_bytes' => 34,
- 'max_payload_bytes' => 11,
- ),
- $this->summarize_xml_content_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 9,
- 'tag_count' => 2,
- 'attribute_count' => 2,
- 'text_token_count' => 1,
- 'cdata_count' => 1,
- 'comment_count' => 1,
- 'processing_instruction_count' => 2,
- 'total_attribute_value_bytes' => 5,
- 'max_attribute_value_bytes' => 4,
- 'total_payload_bytes' => 34,
- 'max_payload_bytes' => 11,
- ),
- $this->summarize_xml_content_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
- }
-
- /**
- * Verifies importer-facing inventories match repeated token scans.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_summarizes_import_inventory( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $xml = '- Title
';
- $processor = $this->create_xml_processor( $implementation, $xml );
-
- $this->assertSame(
- array(
- 'token_count' => 11,
- 'tag_count' => 3,
- 'closing_tag_count' => 2,
- 'unique_tag_name_count' => 3,
- 'duplicate_tag_name_count' => 0,
- 'namespaced_tag_count' => 0,
- 'empty_element_count' => 1,
- 'root_level_tag_count' => 1,
- 'nested_tag_count' => 0,
- 'total_tag_depth' => 5,
- 'max_depth' => 2,
- 'leaf_element_count' => 2,
- 'branch_element_count' => 1,
- 'max_child_element_count' => 2,
- 'attribute_count' => 3,
- 'text_token_count' => 1,
- 'cdata_count' => 1,
- 'comment_count' => 1,
- 'processing_instruction_count' => 2,
- 'total_attribute_value_bytes' => 9,
- 'max_attribute_value_bytes' => 4,
- 'total_payload_bytes' => 34,
- 'max_payload_bytes' => 11,
- ),
- $this->summarize_xml_import_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
-
- $processor = $this->create_xml_processor( $implementation, $xml );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 9,
- 'tag_count' => 2,
- 'closing_tag_count' => 2,
- 'unique_tag_name_count' => 2,
- 'duplicate_tag_name_count' => 0,
- 'namespaced_tag_count' => 0,
- 'empty_element_count' => 1,
- 'root_level_tag_count' => 0,
- 'nested_tag_count' => 0,
- 'total_tag_depth' => 4,
- 'max_depth' => 2,
- 'leaf_element_count' => 2,
- 'branch_element_count' => 0,
- 'max_child_element_count' => 0,
- 'attribute_count' => 2,
- 'text_token_count' => 1,
- 'cdata_count' => 1,
- 'comment_count' => 1,
- 'processing_instruction_count' => 2,
- 'total_attribute_value_bytes' => 5,
- 'max_attribute_value_bytes' => 4,
- 'total_payload_bytes' => 34,
- 'max_payload_bytes' => 11,
- ),
- $this->summarize_xml_import_inventory( $processor, $implementation )
- );
- $this->assertTrue( $processor->is_finished() );
- }
-
- /**
- * Verifies chunked tag summaries stay aligned with tag-by-tag reads.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_tag_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- '- Text
'
- );
- $rows = array();
-
- do {
- $batch = $this->next_xml_tag_summary_batch( $processor, $implementation, 2, 'id' );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertCount( 3, $rows );
- $this->assertSame(
- array( 'root', 'item', 'empty' ),
- array_column( $rows, 'tag_local_name' )
- );
- $this->assertSame(
- array( 'root', '7', null ),
- array_column( $rows, 'id' )
- );
- $this->assertSame( 1, $rows[0]['current_depth'] );
- $this->assertSame( 2, $rows[1]['current_depth'] );
- $this->assertTrue( $rows[2]['is_empty_element'] );
- }
-
- /**
- * Verifies matching tag summaries skip non-matching tags while preserving metadata.
- *
- * @dataProvider data_xml_processor_implementations
- *
- * @param string $implementation Implementation identifier.
- */
- public function test_xml_processor_reads_matching_tag_summary_batches( $implementation ) {
- $this->skip_if_native_is_unavailable( $implementation );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'Title '
- );
- $rows = array();
-
- do {
- $batch = $this->next_xml_matching_tag_summary_batch( $processor, $implementation, 1, 'https://wordpress.org', 'item', 'id' );
- $rows = array_merge( $rows, $batch );
- } while ( ! empty( $batch ) );
-
- $this->assertCount( 2, $rows );
- $this->assertSame(
- array( 'item', 'item' ),
- array_column( $rows, 'tag_local_name' )
- );
- $this->assertSame(
- array( 'https://wordpress.org', 'https://wordpress.org' ),
- array_column( $rows, 'tag_namespace' )
- );
- $this->assertSame(
- array( '7', '8' ),
- array_column( $rows, 'id' )
- );
- $this->assertSame( 2, $rows[0]['current_depth'] );
- $this->assertSame( 2, $rows[1]['current_depth'] );
- $this->assertFalse( $rows[0]['is_empty_element'] );
- $this->assertTrue( $rows[1]['is_empty_element'] );
-
- $this->assertSame(
- array(),
- $this->next_xml_matching_tag_summary_batch( $processor, $implementation, 1, 'https://wordpress.org', 'missing', 'id' )
- );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'Title '
- );
-
- $this->assertSame(
- array(
- 'token_count' => 3,
- 'tag_count' => 1,
- 'attribute_count' => 1,
- ),
- $this->next_xml_matching_tag_count_batch( $processor, $implementation, 1, 'https://wordpress.org', 'item', 'id' )
- );
- $this->assertSame(
- array(
- 'token_count' => 6,
- 'tag_count' => 1,
- 'attribute_count' => 1,
- ),
- $this->next_xml_matching_tag_count_batch( $processor, $implementation, 1, 'https://wordpress.org', 'item', 'id' )
- );
- $this->assertSame(
- array(
- 'token_count' => 1,
- 'tag_count' => 0,
- 'attribute_count' => 0,
- ),
- $this->next_xml_matching_tag_count_batch( $processor, $implementation, 1, 'https://wordpress.org', 'item', 'id' )
- );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'Title '
- );
-
- $this->assertSame(
- array(
- 'token_count' => 10,
- 'tag_count' => 2,
- 'attribute_count' => 2,
- ),
- $this->summarize_xml_matching_tag_stream( $processor, $implementation, 'https://wordpress.org', 'item', 'id' )
- );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'Title '
- );
- $this->assertTrue( $processor->next_tag() );
- $this->assertSame(
- array(
- 'token_count' => 8,
- 'tag_count' => 2,
- 'attribute_count' => 2,
- ),
- $this->summarize_xml_matching_tag_stream( $processor, $implementation, 'https://wordpress.org', 'item', 'id' )
- );
-
- $processor = $this->create_xml_processor(
- $implementation,
- 'Title '
- );
-
- $this->assertSame(
- array(
- 'token_count' => 10,
- 'tag_count' => 2,
- 'attribute_count' => 4,
- ),
- $this->summarize_xml_matching_tag_attributes_stream(
- $processor,
- $implementation,
- 'https://wordpress.org',
- 'item',
- array( 'id', 'slug', 'status', 'missing' )
- )
- );
- }
-
- /**
- * Data provider.
- *
- * @return array[]
- */
- public static function data_xml_processor_implementations() {
- return array(
- 'php-xml-processor' => array( 'php-xml-processor' ),
- 'native-xml-processor' => array( 'native-xml-processor' ),
- );
- }
-
- /**
- * Data provider.
- *
- * @return array[]
- */
- public static function data_xml_without_document_element() {
- return array(
- 'php-empty' => array( '', 'php-xml-processor' ),
- 'native-empty' => array( '', 'native-xml-processor' ),
- 'php-whitespace' => array( " \n\t", 'php-xml-processor' ),
- 'native-whitespace' => array( " \n\t", 'native-xml-processor' ),
- 'php-comment-only' => array( '', 'php-xml-processor' ),
- 'native-comment-only' => array( '', 'native-xml-processor' ),
- 'php-declaration-only' => array( '', 'php-xml-processor' ),
- 'native-declaration-only' => array( '', 'native-xml-processor' ),
- );
- }
-
- /**
- * Data provider.
- *
- * @return array[]
- */
- public static function data_xml_with_leading_misc_whitespace() {
- return array(
- 'php-whitespace-root' => array( " \n\t ", array( 'root' ), 'php-xml-processor' ),
- 'native-whitespace-root' => array( " \n\t ", array( 'root' ), 'native-xml-processor' ),
- 'php-comment-whitespace-root' => array( "\n ", array( '#comment', 'root' ), 'php-xml-processor' ),
- 'native-comment-whitespace-root' => array( "\n ", array( '#comment', 'root' ), 'native-xml-processor' ),
- 'php-declaration-whitespace-root' => array( "\n ", array( '#xml-declaration', 'root' ), 'php-xml-processor' ),
- 'native-declaration-whitespace-root' => array( "\n ", array( '#xml-declaration', 'root' ), 'native-xml-processor' ),
- );
- }
-
- /**
- * Data provider.
- *
- * @return array[]
- */
- public static function data_malformed_attribute_xml_processor_implementations() {
- return array(
- 'php-lt-in-attribute-value' => array( ' ', 'php-xml-processor' ),
- 'native-lt-in-attribute-value' => array( ' ', 'native-xml-processor' ),
- 'php-duplicate-attribute' => array( ' ', 'php-xml-processor' ),
- 'native-duplicate-attribute' => array( ' ', 'native-xml-processor' ),
- 'php-empty-prefixed-namespace' => array( ' ', 'php-xml-processor' ),
- 'native-empty-prefixed-namespace' => array( ' ', 'native-xml-processor' ),
- );
- }
-
- /**
- * Creates an XML processor for a specific implementation.
- *
- * @param string $implementation Implementation identifier.
- * @param string $xml XML input.
- * @return object Processor instance.
- */
- private function create_xml_processor( $implementation, $xml ) {
- if ( 'native-xml-processor' === $implementation ) {
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
-
- return $class_name::create_from_string( $xml );
- }
-
- $processor = XMLProcessor::create_from_string( $xml );
- $this->disable_native_delegate( $processor );
-
- return $processor;
- }
-
- /**
- * Creates a streaming XML processor for a specific implementation.
- *
- * @param string $implementation Implementation identifier.
- * @param string $xml XML input.
- * @return object Processor instance.
- */
- private function create_xml_streaming_processor( $implementation, $xml ) {
- if ( 'native-xml-processor' === $implementation ) {
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
-
- return $class_name::create_for_streaming( $xml, null, 'UTF-8', array() );
- }
-
- $processor = XMLProcessor::create_for_streaming( $xml );
- $this->disable_native_delegate( $processor );
-
- return $processor;
- }
-
- /**
- * Creates a streaming XML processor from a reentrancy cursor for a specific implementation.
- *
- * @param string $implementation Implementation identifier.
- * @param string $xml XML input.
- * @param string $cursor Reentrancy cursor.
- * @return object Processor instance.
- */
- private function create_xml_streaming_processor_from_cursor( $implementation, $xml, $cursor ) {
- if ( 'native-xml-processor' === $implementation ) {
- $class_name = 'WordPress\\XML\\NativeXMLProcessor';
-
- return $class_name::create_for_streaming( $xml, $cursor, 'UTF-8', array() );
- }
-
- $processor = XMLProcessor::create_for_streaming( $xml, $cursor );
- $this->disable_native_delegate( $processor );
-
- return $processor;
- }
-
- /**
- * Disables the native delegate so PHP implementation rows still cover PHP.
- *
- * @param object $processor Processor instance.
- */
- private function disable_native_delegate( $processor ) {
- if ( method_exists( $processor, 'disable_native_processor' ) ) {
- $processor->disable_native_processor();
- }
- }
-
- /**
- * Returns the native delegate for public-class default checks.
- *
- * @param object $processor Processor instance.
- * @return object|null Native delegate.
- */
- private function get_native_delegate( $processor ) {
- if (
- class_exists( 'WordPress\\XML\\NativeXMLProcessor', false ) &&
- ! method_exists( 'WordPress\\XML\\NativeXMLProcessor', 'supports_public_api' )
- ) {
- $this->markTestSkipped( 'WordPress\\XML\\NativeXMLProcessor does not expose the public API surface; rebuild the native extension.' );
- }
-
- if ( method_exists( $processor, 'get_native_processor' ) ) {
- return $processor->get_native_processor();
- }
-
- return null;
- }
-
- /**
- * Runs a fresh PHP process that defines native-default constants before bootstrap.
- *
- * @param string $constant_definitions PHP source defining constants.
- * @return string[] Probe output lines.
- */
- private function run_xml_native_defaults_constant_probe( $constant_definitions ) {
- $root = dirname( __DIR__, 3 );
- $extension = $root . '/extensions/native-apis/target/release/libwp_native_apis.so';
-
- if ( ! file_exists( $extension ) ) {
- $this->markTestSkipped( 'Native API extension binary is not built.' );
- }
-
- $code = $constant_definitions . "\n" .
- 'require ' . var_export( $root . '/bootstrap.php', true ) . ";\n" .
- '$class_name = \'WordPress\\\\XML\\\\XMLProcessor\';' . "\n" .
- '$processor = $class_name::create_from_string( \' \' );' . "\n" .
- '$delegate = method_exists( $processor, \'get_native_processor\' ) ? $processor->get_native_processor() : null;' . "\n" .
- 'echo \'delegate:\' . ( null === $delegate ? \'php\' : get_class( $delegate ) ) . "\n";' . "\n" .
- '$processor->next_tag( \'item\' );' . "\n" .
- 'echo \'token:\' . $processor->get_token_name() . "\n";' . "\n" .
- 'echo \'id:\' . $processor->get_attribute( \'\', \'id\' ) . "\n";';
-
- $command = escapeshellarg( PHP_BINARY ) . ' -d extension=' . escapeshellarg( $extension ) . ' -r ' . escapeshellarg( $code );
- $output = array();
- $status = 0;
- exec( $command . ' 2>&1', $output, $status );
-
- $this->assertSame( 0, $status, implode( "\n", $output ) );
-
- return $output;
- }
-
- /**
- * Reads an XML attribute through the implementation-specific signature.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $local_name Attribute local name.
- * @return string|true|null Attribute value.
- */
- private function get_xml_attribute( $processor, $implementation, $local_name, $namespace = '' ) {
- return $processor->get_attribute( $namespace, $local_name );
- }
-
- /**
- * Skips implementation rows when an API is provided only by the native class.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $method Method name.
- */
- private function skip_if_xml_method_is_unavailable( $processor, $implementation, $method ) {
- if ( ! method_exists( $processor, $method ) ) {
- $this->markTestSkipped( "{$method} is not exposed by {$implementation}." );
- }
- }
-
- /**
- * Runs the implementation-specific XML attribute-prefix summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string|null $full_namespace_prefix Namespace prefix to match.
- * @param string $local_name_prefix Local name prefix to match.
- * @return array Summary with `tag_count` and `attribute_count`.
- */
- private function summarize_xml_attribute_names_with_prefix( $processor, $implementation, $full_namespace_prefix, $local_name_prefix ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_attribute_names_with_prefix' );
-
- $summary = $processor->summarize_attribute_names_with_prefix( $full_namespace_prefix, $local_name_prefix );
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 3 );
-
- return array(
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- );
- }
-
- /**
- * Runs the implementation-specific XML token stream summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $attribute_name Attribute name to count.
- * @return array Summary with `token_count`, `tag_count`, and `attribute_count`.
- */
- private function summarize_xml_token_stream( $processor, $implementation, $attribute_name ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_token_stream' );
-
- $summary = $processor->summarize_token_stream( $attribute_name );
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 3 );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- );
- }
-
- /**
- * Runs the implementation-specific XML document inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with token category counts and maximum depth.
- */
- private function summarize_xml_document_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_document_inventory' );
-
- $summary = $processor->summarize_document_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 8 );
- $this->assertCount( 8, $parts, 'Expected compact XML document inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'closing_tag_count' => (int) $parts[2],
- 'text_token_count' => (int) $parts[3],
- 'comment_count' => (int) $parts[4],
- 'cdata_count' => (int) $parts[5],
- 'max_depth' => (int) $parts[6],
- 'empty_element_count' => (int) $parts[7],
- );
- }
-
- /**
- * Runs the implementation-specific XML element inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with element-name inventory counts.
- */
- private function summarize_xml_element_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_element_inventory' );
-
- $summary = $processor->summarize_element_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 7 );
- $this->assertCount( 7, $parts, 'Expected compact XML element inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'closing_tag_count' => (int) $parts[2],
- 'unique_tag_name_count' => (int) $parts[3],
- 'duplicate_tag_name_count' => (int) $parts[4],
- 'namespaced_tag_count' => (int) $parts[5],
- 'empty_element_count' => (int) $parts[6],
- );
- }
-
- /**
- * Runs the implementation-specific XML depth inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with depth distribution counts.
- */
- private function summarize_xml_depth_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_depth_inventory' );
-
- $summary = $processor->summarize_depth_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 8 );
- $this->assertCount( 8, $parts, 'Expected compact XML depth inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'closing_tag_count' => (int) $parts[2],
- 'empty_element_count' => (int) $parts[3],
- 'root_level_tag_count' => (int) $parts[4],
- 'nested_tag_count' => (int) $parts[5],
- 'total_tag_depth' => (int) $parts[6],
- 'max_depth' => (int) $parts[7],
- );
- }
-
- /**
- * Runs the implementation-specific XML leaf inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with leaf and branch element counts.
- */
- private function summarize_xml_leaf_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_leaf_inventory' );
-
- $summary = $processor->summarize_leaf_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 7 );
- $this->assertCount( 7, $parts, 'Expected compact XML leaf inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'closing_tag_count' => (int) $parts[2],
- 'empty_element_count' => (int) $parts[3],
- 'leaf_element_count' => (int) $parts[4],
- 'branch_element_count' => (int) $parts[5],
- 'max_child_element_count' => (int) $parts[6],
- );
- }
-
- /**
- * Runs the implementation-specific XML structural inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with structural element counts.
- */
- private function summarize_xml_structural_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_structural_inventory' );
-
- $summary = $processor->summarize_structural_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 14 );
- $this->assertCount( 14, $parts, 'Expected compact XML structural inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'closing_tag_count' => (int) $parts[2],
- 'unique_tag_name_count' => (int) $parts[3],
- 'duplicate_tag_name_count' => (int) $parts[4],
- 'namespaced_tag_count' => (int) $parts[5],
- 'empty_element_count' => (int) $parts[6],
- 'root_level_tag_count' => (int) $parts[7],
- 'nested_tag_count' => (int) $parts[8],
- 'total_tag_depth' => (int) $parts[9],
- 'max_depth' => (int) $parts[10],
- 'leaf_element_count' => (int) $parts[11],
- 'branch_element_count' => (int) $parts[12],
- 'max_child_element_count' => (int) $parts[13],
- );
- }
-
- /**
- * Runs the implementation-specific XML attribute inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with attribute inventory counts.
- */
- private function summarize_xml_attribute_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_attribute_inventory' );
-
- $summary = $processor->summarize_attribute_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- $this->assertCount( 6, $parts, 'Expected compact XML attribute inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- 'namespaced_attribute_count' => (int) $parts[3],
- 'tags_with_attributes_count' => (int) $parts[4],
- 'max_attribute_count' => (int) $parts[5],
- );
- }
-
- /**
- * Runs the implementation-specific XML ID inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with ID inventory counts.
- */
- private function summarize_xml_id_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_id_inventory' );
-
- $summary = $processor->summarize_id_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- $this->assertCount( 6, $parts, 'Expected compact XML ID inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'id_attribute_count' => (int) $parts[2],
- 'unique_id_count' => (int) $parts[3],
- 'duplicate_id_count' => (int) $parts[4],
- 'id_value_bytes' => (int) $parts[5],
- );
- }
-
- /**
- * Runs the implementation-specific XML namespace inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with namespace inventory counts.
- */
- private function summarize_xml_namespace_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_namespace_inventory' );
-
- $summary = $processor->summarize_namespace_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- $this->assertCount( 6, $parts, 'Expected compact XML namespace inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'namespaced_tag_count' => (int) $parts[2],
- 'attribute_count' => (int) $parts[3],
- 'namespaced_attribute_count' => (int) $parts[4],
- 'unique_namespace_count' => (int) $parts[5],
- );
- }
-
- /**
- * Runs the implementation-specific XML text inventory summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with text and CDATA inventory counts.
- */
- private function summarize_xml_text_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_text_inventory' );
-
- $summary = $processor->summarize_text_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 7 );
- $this->assertCount( 7, $parts, 'Expected compact XML text inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'text_token_count' => (int) $parts[1],
- 'cdata_count' => (int) $parts[2],
- 'non_empty_text_count' => (int) $parts[3],
- 'whitespace_text_count' => (int) $parts[4],
- 'total_text_bytes' => (int) $parts[5],
- 'max_text_bytes' => (int) $parts[6],
- );
- }
-
- /**
- * Runs the implementation-specific XML processing instruction inventory API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with XML declaration and processing instruction counts.
- */
- private function summarize_xml_processing_instruction_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_processing_instruction_inventory' );
-
- $summary = $processor->summarize_processing_instruction_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- $this->assertCount( 6, $parts, 'Expected compact XML processing instruction inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'processing_instruction_count' => (int) $parts[1],
- 'xml_declaration_count' => (int) $parts[2],
- 'non_empty_instruction_count' => (int) $parts[3],
- 'total_instruction_bytes' => (int) $parts[4],
- 'max_instruction_bytes' => (int) $parts[5],
- );
- }
-
- /**
- * Runs the implementation-specific XML comment inventory API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with XML comment counts and byte totals.
- */
- private function summarize_xml_comment_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_comment_inventory' );
-
- $summary = $processor->summarize_comment_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 6 );
- $this->assertCount( 6, $parts, 'Expected compact XML comment inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'comment_count' => (int) $parts[1],
- 'non_empty_comment_count' => (int) $parts[2],
- 'empty_comment_count' => (int) $parts[3],
- 'total_comment_bytes' => (int) $parts[4],
- 'max_comment_bytes' => (int) $parts[5],
- );
- }
-
- /**
- * Runs the implementation-specific XML payload inventory API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with XML payload counts and byte totals.
- */
- private function summarize_xml_payload_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_payload_inventory' );
-
- $summary = $processor->summarize_payload_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 7 );
- $this->assertCount( 7, $parts, 'Expected compact XML payload inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'text_token_count' => (int) $parts[1],
- 'cdata_count' => (int) $parts[2],
- 'comment_count' => (int) $parts[3],
- 'processing_instruction_count' => (int) $parts[4],
- 'total_payload_bytes' => (int) $parts[5],
- 'max_payload_bytes' => (int) $parts[6],
- );
- }
-
- /**
- * Runs the implementation-specific XML content inventory API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with XML content and metadata byte totals.
- */
- private function summarize_xml_content_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_content_inventory' );
-
- $summary = $processor->summarize_content_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 11 );
- $this->assertCount( 11, $parts, 'Expected compact XML content inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- 'text_token_count' => (int) $parts[3],
- 'cdata_count' => (int) $parts[4],
- 'comment_count' => (int) $parts[5],
- 'processing_instruction_count' => (int) $parts[6],
- 'total_attribute_value_bytes' => (int) $parts[7],
- 'max_attribute_value_bytes' => (int) $parts[8],
- 'total_payload_bytes' => (int) $parts[9],
- 'max_payload_bytes' => (int) $parts[10],
- );
- }
-
- /**
- * Runs the implementation-specific XML import inventory API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @return array Summary with importer-facing XML structure and content counts.
- */
- private function summarize_xml_import_inventory( $processor, $implementation ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_import_inventory' );
-
- $summary = $processor->summarize_import_inventory();
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 23 );
- $this->assertCount( 23, $parts, 'Expected compact XML import inventory summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'closing_tag_count' => (int) $parts[2],
- 'unique_tag_name_count' => (int) $parts[3],
- 'duplicate_tag_name_count' => (int) $parts[4],
- 'namespaced_tag_count' => (int) $parts[5],
- 'empty_element_count' => (int) $parts[6],
- 'root_level_tag_count' => (int) $parts[7],
- 'nested_tag_count' => (int) $parts[8],
- 'total_tag_depth' => (int) $parts[9],
- 'max_depth' => (int) $parts[10],
- 'leaf_element_count' => (int) $parts[11],
- 'branch_element_count' => (int) $parts[12],
- 'max_child_element_count' => (int) $parts[13],
- 'attribute_count' => (int) $parts[14],
- 'text_token_count' => (int) $parts[15],
- 'cdata_count' => (int) $parts[16],
- 'comment_count' => (int) $parts[17],
- 'processing_instruction_count' => (int) $parts[18],
- 'total_attribute_value_bytes' => (int) $parts[19],
- 'max_attribute_value_bytes' => (int) $parts[20],
- 'total_payload_bytes' => (int) $parts[21],
- 'max_payload_bytes' => (int) $parts[22],
- );
- }
-
- /**
- * Runs the implementation-specific XML tag stream summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $attribute_name Attribute name to count.
- * @return array Summary with `tag_count` and `attribute_count`.
- */
- private function summarize_xml_tag_stream( $processor, $implementation, $attribute_name ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_tag_stream' );
-
- $summary = $processor->summarize_tag_stream( $attribute_name );
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 3 );
-
- return array(
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- );
- }
-
- /**
- * Runs the implementation-specific XML token summary batch API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param int $max_tokens Maximum number of tokens to fetch.
- * @return array[] Token summary rows.
- */
- private function next_xml_token_summary_batch( $processor, $implementation, $max_tokens ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'next_token_summary_batch' );
-
- return $processor->next_token_summary_batch( $max_tokens );
- }
-
- /**
- * Runs the implementation-specific XML tag summary batch API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param int $max_tags Maximum number of tags to fetch.
- * @param string $attribute_name Attribute name to include.
- * @return array[] Tag summary rows.
- */
- private function next_xml_tag_summary_batch( $processor, $implementation, $max_tags, $attribute_name ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'next_tag_summary_batch' );
-
- return $processor->next_tag_summary_batch( $max_tags, $attribute_name );
- }
-
- /**
- * Runs the implementation-specific XML matching tag summary batch API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param int $max_tags Maximum number of matching tags to fetch.
- * @param string $tag_namespace Namespace URI to match.
- * @param string $tag_local_name Local tag name to match.
- * @param string $attribute_name Attribute name to include.
- * @return array[] Tag summary rows.
- */
- private function next_xml_matching_tag_summary_batch( $processor, $implementation, $max_tags, $tag_namespace, $tag_local_name, $attribute_name ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'next_matching_tag_summary_batch' );
-
- return $processor->next_matching_tag_summary_batch( $max_tags, $tag_namespace, $tag_local_name, $attribute_name );
- }
-
- /**
- * Runs the implementation-specific XML matching tag count batch API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param int $max_tags Maximum number of matching tags to count.
- * @param string $tag_namespace Namespace URI to match.
- * @param string $tag_local_name Local tag name to match.
- * @param string $attribute_name Attribute name to count.
- * @return array Summary with `token_count`, `tag_count`, and `attribute_count`.
- */
- private function next_xml_matching_tag_count_batch( $processor, $implementation, $max_tags, $tag_namespace, $tag_local_name, $attribute_name ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'next_matching_tag_count_batch' );
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $processor->next_matching_tag_count_batch( $max_tags, $tag_namespace, $tag_local_name, $attribute_name );
- }
-
- $summary = $processor->next_matching_tag_count_batch( $max_tags, $tag_namespace, $tag_local_name, $attribute_name );
- if ( ! is_string( $summary ) ) {
- return array(
- 'token_count' => 0,
- 'tag_count' => 0,
- 'attribute_count' => 0,
- );
- }
-
- $parts = explode( "\x1f", $summary, 3 );
- $this->assertCount( 3, $parts, 'Expected compact XML matching tag count batch summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- );
- }
-
- /**
- * Runs the implementation-specific XML matching tag summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $tag_namespace Namespace URI to match.
- * @param string $tag_local_name Local tag name to match.
- * @param string $attribute_name Attribute name to count.
- * @return array Summary with `token_count`, `tag_count`, and `attribute_count`.
- */
- private function summarize_xml_matching_tag_stream( $processor, $implementation, $tag_namespace, $tag_local_name, $attribute_name ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_matching_tag_stream' );
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $processor->summarize_matching_tag_stream( $tag_namespace, $tag_local_name, $attribute_name );
- }
-
- $summary = $processor->summarize_matching_tag_stream( $tag_namespace, $tag_local_name, $attribute_name );
- $this->assertIsString( $summary );
-
- $parts = explode( "\x1f", $summary, 3 );
- $this->assertCount( 3, $parts, 'Expected compact XML matching tag stream summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- );
- }
-
- /**
- * Runs the implementation-specific XML matching tag attributes summary API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $tag_namespace Namespace URI to match.
- * @param string $tag_local_name Local tag name to match.
- * @param array $attribute_names Attribute names to count.
- * @return array Summary with `token_count`, `tag_count`, and `attribute_count`.
- */
- private function summarize_xml_matching_tag_attributes_stream( $processor, $implementation, $tag_namespace, $tag_local_name, $attribute_names ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'summarize_matching_tag_attributes_stream' );
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $processor->summarize_matching_tag_attributes_stream( $tag_namespace, $tag_local_name, $attribute_names );
- }
-
- $summary = $processor->summarize_matching_tag_attributes_stream(
- $tag_namespace,
- $tag_local_name,
- implode( "\x1f", $attribute_names )
- );
- $this->assertIsString( $summary );
-
- $parts = explode( "\x1f", $summary, 3 );
- $this->assertCount( 3, $parts, 'Expected compact XML matching tag attributes stream summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- );
- }
-
- /**
- * Runs the implementation-specific XML tag count batch API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param int $max_tags Maximum number of tags to count.
- * @param string $attribute_name Attribute name to count.
- * @return array Summary with `token_count`, `tag_count`, and `attribute_count`.
- */
- private function next_xml_tag_count_batch( $processor, $implementation, $max_tags, $attribute_name ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'next_tag_count_batch' );
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $processor->next_tag_count_batch( $max_tags, $attribute_name );
- }
-
- $summary = $processor->next_tag_count_batch( $max_tags, $attribute_name );
- if ( ! is_string( $summary ) ) {
- return array(
- 'token_count' => 0,
- 'tag_count' => 0,
- 'attribute_count' => 0,
- );
- }
-
- $parts = explode( "\x1f", $summary, 3 );
- $this->assertCount( 3, $parts, 'Expected compact XML tag count batch summary.' );
-
- return array(
- 'token_count' => (int) $parts[0],
- 'tag_count' => (int) $parts[1],
- 'attribute_count' => (int) $parts[2],
- );
- }
-
- /**
- * Runs a streaming XML batch case.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string $case Batch case identifier.
- * @return mixed Batch result.
- */
- private function run_xml_streaming_batch_case( $processor, $implementation, $case ) {
- switch ( $case ) {
- case 'token_compact':
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'next_token_compact_summary_batch' );
- return $processor->next_token_compact_summary_batch( 10 );
-
- case 'tag_compact':
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'next_tag_compact_summary_batch' );
- return $processor->next_tag_compact_summary_batch( 10, 'id' );
-
- case 'tag_count':
- return $this->next_xml_tag_count_batch( $processor, $implementation, 10, 'id' );
-
- case 'matching_tag_compact':
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'next_matching_tag_compact_summary_batch' );
- return $processor->next_matching_tag_compact_summary_batch( 10, '', 'item', 'id' );
-
- case 'matching_tag_count':
- return $this->next_xml_matching_tag_count_batch( $processor, $implementation, 10, '', 'item', 'id' );
- }
-
- $this->fail( 'Unknown XML streaming batch case: ' . $case );
- }
-
- /**
- * Parses a native compact XML token summary row.
- *
- * @param string $metadata Native compact metadata row.
- * @return array Token summary row.
- */
- private function parse_native_compact_token_summary( $metadata ) {
- $parts = explode( "\x1f", $metadata, 6 );
- $this->assertCount( 6, $parts, 'Expected compact XML token summary row.' );
-
- switch ( $parts[0] ) {
- case 't':
- $token_type = '#tag';
- $token_name = $parts[1];
- break;
- case 'x':
- $token_type = '#xml-declaration';
- $token_name = '#xml-declaration';
- break;
- case 'd':
- $token_type = '#doctype';
- $token_name = '#doctype';
- break;
- case 'p':
- $token_type = '#processing-instructions';
- $token_name = '#processing-instructions';
- break;
- case 'c':
- $token_type = '#comment';
- $token_name = '#comment';
- break;
- case 'a':
- $token_type = '#cdata-section';
- $token_name = '#cdata-section';
- break;
- default:
- $token_type = '#text';
- $token_name = '#text';
- break;
- }
-
- $id_found = isset( $parts[5][0] ) && '1' === $parts[5][0];
-
- return array(
- 'token_type' => $token_type,
- 'token_name' => $token_name,
- 'tag_local_name' => '#tag' === $token_type ? $parts[1] : '',
- 'tag_namespace' => '#tag' === $token_type ? $parts[2] : '',
- 'tag_namespace_and_local_name' => '#tag' === $token_type && '' !== $parts[2] ? '{' . $parts[2] . '}' . $parts[1] : ( '#tag' === $token_type ? $parts[1] : '' ),
- 'is_tag_closer' => isset( $parts[3][0] ) && '1' === $parts[3][0],
- 'is_empty_element' => isset( $parts[3][1] ) && '1' === $parts[3][1],
- 'current_depth' => (int) $parts[4],
- 'id' => $id_found ? substr( $parts[5], 1 ) : null,
- );
- }
-
- /**
- * Parses a native compact XML tag summary row.
- *
- * @param string $metadata Native compact metadata row.
- * @param string $attribute_name Attribute name included in the row.
- * @return array Tag summary row.
- */
- private function parse_native_compact_tag_summary( $metadata, $attribute_name ) {
- $parts = explode( "\x1f", $metadata, 6 );
- $this->assertCount( 6, $parts, 'Expected compact XML tag summary row.' );
- $this->assertGreaterThan( 0, (int) $parts[0], 'Expected compact XML tag summary to include consumed token count.' );
-
- $attribute_found = isset( $parts[5][0] ) && '1' === $parts[5][0];
-
- return array(
- 'tag_local_name' => $parts[1],
- 'tag_namespace' => $parts[2],
- 'tag_namespace_and_local_name' => '' === $parts[2] ? $parts[1] : '{' . $parts[2] . '}' . $parts[1],
- 'is_empty_element' => isset( $parts[3][0] ) && '1' === $parts[3][0],
- 'current_depth' => (int) $parts[4],
- $attribute_name => $attribute_found ? substr( $parts[5], 1 ) : null,
- );
- }
-
- /**
- * Runs the implementation-specific XML attribute-prefix document removal API.
- *
- * @param object $processor Processor instance.
- * @param string $implementation Implementation identifier.
- * @param string|null $full_namespace_prefix Namespace prefix to match.
- * @param string $local_name_prefix Local name prefix to match.
- * @return array Summary with `tag_count`, `removed_count`, and `xml`.
- */
- private function remove_xml_attribute_names_with_prefix_from_document( $processor, $implementation, $full_namespace_prefix, $local_name_prefix ) {
- $this->skip_if_xml_method_is_unavailable( $processor, $implementation, 'remove_attributes_with_prefix_from_document' );
-
- $summary = $processor->remove_attributes_with_prefix_from_document( $full_namespace_prefix, $local_name_prefix );
-
- if ( 'native-xml-processor' !== $implementation ) {
- return $summary;
- }
-
- $parts = explode( "\x1f", $summary, 3 );
-
- return array(
- 'tag_count' => (int) $parts[0],
- 'removed_count' => (int) $parts[1],
- 'xml' => $parts[2],
- );
- }
-
- /**
- * Skips native implementation cases when the extension is unavailable.
- *
- * @param string $implementation Implementation identifier.
- */
- private function skip_if_native_is_unavailable( $implementation ) {
- if ( 'native-xml-processor' === $implementation && ! class_exists( 'WordPress\\XML\\NativeXMLProcessor', false ) ) {
- $this->markTestSkipped( 'WordPress\\XML\\NativeXMLProcessor is not registered; load the native API extension to run this case.' );
- }
- }
-}
diff --git a/components/XML/class-xmlnativecursorprocessor.php b/components/XML/class-xmlnativecursorprocessor.php
deleted file mode 100644
index e11d098e9..000000000
--- a/components/XML/class-xmlnativecursorprocessor.php
+++ /dev/null
@@ -1,12 +0,0 @@
-
PHP Toolkit — PHP libraries for WordPress and the open web
-
+
@@ -49,16 +48,6 @@ Where to start
-
-
The problem
diff --git a/docs/learn/01-rewriting-html.html b/docs/learn/01-rewriting-html.html
index 8afdc9572..e601293f9 100644
--- a/docs/learn/01-rewriting-html.html
+++ b/docs/learn/01-rewriting-html.html
@@ -5,7 +5,7 @@
Chapter 1 — Rewriting HTML safely · PHP Toolkit
-
+
@@ -16,7 +16,6 @@
Learn
Reference
- Native APIs
GitHub
diff --git a/docs/learn/02-streaming-archives.html b/docs/learn/02-streaming-archives.html
index 1a788b78d..e2ee36b44 100644
--- a/docs/learn/02-streaming-archives.html
+++ b/docs/learn/02-streaming-archives.html
@@ -5,7 +5,7 @@
Chapter 2 — Streaming archives · PHP Toolkit
-
+
@@ -16,7 +16,6 @@
Learn
Reference
- Native APIs
GitHub
diff --git a/docs/learn/03-importing-content.html b/docs/learn/03-importing-content.html
index e8b6ed3de..c44c19da4 100644
--- a/docs/learn/03-importing-content.html
+++ b/docs/learn/03-importing-content.html
@@ -5,7 +5,7 @@
Chapter 3 — Markdown to WXR · PHP Toolkit
-
+
@@ -16,7 +16,6 @@
Learn
Reference
- Native APIs
GitHub
diff --git a/docs/learn/04-talking-to-the-network.html b/docs/learn/04-talking-to-the-network.html
index e99bfc1f1..f2cfcf8d0 100644
--- a/docs/learn/04-talking-to-the-network.html
+++ b/docs/learn/04-talking-to-the-network.html
@@ -5,7 +5,7 @@
Chapter 4 — Talking to the network · PHP Toolkit
-
+
@@ -16,7 +16,6 @@
Learn
Reference
- Native APIs
GitHub
diff --git a/docs/learn/index.html b/docs/learn/index.html
index 56c549633..b2ae0f290 100644
--- a/docs/learn/index.html
+++ b/docs/learn/index.html
@@ -5,7 +5,7 @@
Learn — PHP Toolkit
-
+
diff --git a/docs/learn/quickstart.html b/docs/learn/quickstart.html
index f4c24feb4..792fc7188 100644
--- a/docs/learn/quickstart.html
+++ b/docs/learn/quickstart.html
@@ -5,7 +5,7 @@
Quickstart — PHP Toolkit
-
+
@@ -16,7 +16,6 @@
Learn
Reference
- Native APIs
GitHub
diff --git a/docs/learn/recap.html b/docs/learn/recap.html
index 15140e14d..ca682d071 100644
--- a/docs/learn/recap.html
+++ b/docs/learn/recap.html
@@ -5,7 +5,7 @@
Recap · PHP Toolkit
-
+
diff --git a/docs/native-apis.html b/docs/native-apis.html
deleted file mode 100644
index 4af08c866..000000000
--- a/docs/native-apis.html
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-
-
-
-Native APIs — PHP Toolkit
-
-
-
-
-
-
-
-
-
- Experimental performance preview
- 10x the php-toolkit speed
- Try the experimental wp_native_apis extension in WordPress Playground first. It loads before PHP starts, confirms the WASM extension was registered, and shows quick browser-side timing numbers for hot WP_HTML_Tag_Processor, XMLProcessor, and URLInTextProcessor paths.
-
-
-
- Quick start
-
-
- Try in Playground web
- Open a ready-made Playground that loads the latest PHP.wasm manifest with php-extension, installs the smoke-test Blueprint, and lands on /native-api-smoke.php.
- Test in Playground →
-
-
- Try in Playground CLI
- Use the same manifest locally. The extension flag must be passed before PHP starts.
- npx @wp-playground/cli@latest server \
- --php=8.5 \
- --php-extension=https://wordpress.github.io/php-toolkit/wp_native_apis-wasm-extension/latest/manifest.json \
- --blueprint=https://raw.githubusercontent.com/WordPress/php-toolkit/trunk/extensions/native-apis/playground/blueprint.json
-
-
-
- What you should see: the Playground page says wp_native_apis WASM extension loaded, then shows whether native classes are available and a few quick timing cards.
-
-
-
-
- What this accelerates
-
-
- WP_HTML_Tag_Processor
- Fast scans and aggregate checks for common tag-processor and fragment-processor workloads.
- Reference →
-
-
- XMLProcessor
- Streaming token, tag, namespace, attribute, and inventory summaries without building a DOM tree.
- Reference →
-
-
- URLInTextProcessor
- A native candidate scanner for plain-text URL detection, with public handling still validating through the existing parser path.
- Reference →
-
-
-
-
-
- Status and fallback behavior
-
- Experimental: the packaging, release cadence, and accelerated surface are still evolving. The PHP Toolkit APIs remain safe to use without the extension.
-
- When wp_native_apis is loaded before the toolkit, wrappers may delegate covered operations to native classes. If the extension is missing, incompatible, or disabled with WP_NATIVE_APIS_DISABLE_DEFAULTS, the same public APIs continue through pure-PHP implementations.
-
-
-
- WASM releases for Playground
- The release index is for pinned manifests, checksums, and exact Playground links. Use latest for a quick trial; use a commit-specific manifest when you need a reproducible demo.
- Browse WASM releases for Playground →
-
-
-
- Benchmarks
- The 10x claim comes from CI benchmark snapshots for the host Rust-backed extension. Playground is the easiest way to see that the extension loads and to get quick browser-side timing numbers; host PHP benchmarks are the better signal for Rust-backed throughput.
- php -d extension=extensions/native-apis/target/release/libwp_native_apis.so \
- bin/benchmark-native-apis.php --iterations=50 --mode=both --disable-native-defaults --require-native
- Benchmark numbers are machine- and workflow-run-specific. Treat them as directional evidence for which workflows benefit from native batching, not as universal throughput guarantees.
-
-
-
- Compile for native PHP
- After trying Playground, build the Rust-backed extension for your local host PHP ABI when you want the real native implementation and benchmark numbers. A build made for one PHP ABI cannot be reused for another.
- Build the native PHP extension →
-
-
-
-
-
-
-
diff --git a/docs/native-php-extension.html b/docs/native-php-extension.html
deleted file mode 100644
index 0bd7f8968..000000000
--- a/docs/native-php-extension.html
+++ /dev/null
@@ -1,139 +0,0 @@
-
-
-
-
-
-Compile the Native APIs extension for PHP — PHP Toolkit
-
-
-
-
-
-
-
-
-
- Experimental native PHP build
- Compile wp_native_apis for host PHP.
- Use this guide when you want the Rust-backed Native APIs extension on a local PHP CLI or server. The build is tied to the PHP version and platform you compile against, so treat it as a development and benchmarking path until packaged host releases are published.
-
-
-
-
- What you will build
- The host PHP extension is the Rust-backed implementation built with ext-php-rs. When it is loaded before the toolkit bootstrap, covered WP_HTML_Tag_Processor, XMLProcessor, and URLInTextProcessor classes can delegate hot paths to native code. If the extension is absent, incompatible, or disabled, the public PHP APIs keep using their pure-PHP fallback implementations.
-
- Not the Playground build: WordPress Playground uses a PHP.wasm extension bundle. This page is for native host PHP, such as a local Linux PHP CLI with development headers.
-
-
-
-
- Prerequisites
-
- A checkout of WordPress/php-toolkit .
- The PHP CLI you want to target, plus matching development headers and php-config.
- Rust and Cargo.
- Clang and libclang for Rust bindgen.
- Composer dependencies installed from the repository root.
-
- On Ubuntu, the CI benchmark job installs the native build dependencies like this:
- sudo apt-get update
-sudo apt-get install -y clang libclang-dev
-composer install --prefer-dist --no-progress --no-suggest
- On other systems, install equivalent PHP development headers, Rust, Cargo, Clang, and libclang. If php-config or libclang are not discoverable, pass their paths explicitly in the build step.
-
-
-
- Build the extension
- Run the helper script from the repository root:
- extensions/native-apis/build-extension.sh
- If your target PHP is not the first php-config on PATH, point the build at the exact one you want:
- PHP_CONFIG=/path/to/php-config \
-LIBCLANG_PATH=/path/to/libclang/lib \
-extensions/native-apis/build-extension.sh
- The documented Linux build writes the shared object here:
- extensions/native-apis/target/release/libwp_native_apis.so
- Use the same PHP binary at runtime that matches the php-config used at build time. Native PHP extensions are ABI-specific; do not reuse one shared object across PHP versions.
-
-
-
- Verify that PHP can load it
- Load the extension before running the native verifier:
- php -d extension=extensions/native-apis/target/release/libwp_native_apis.so \
- extensions/native-apis/tests/verify-native-apis.php
- A successful run means PHP loaded the extension and the expected native classes are registered. If the script reports that the extension is unavailable, re-check the PHP version, php-config path, shared-object path, and libclang setup.
-
-
-
- Use it through the public toolkit classes
- Load the extension before the toolkit bootstrap. The public wrappers decide whether a native delegate is available and fall back to PHP when it is not.
- php -d extension=extensions/native-apis/target/release/libwp_native_apis.so <<'PHP'
-<?php
-require __DIR__ . '/bootstrap.php';
-
-var_dump( is_subclass_of( 'WP_HTML_Tag_Processor', 'WP_HTML_Native_Tag_Processor' ) );
-var_dump( is_subclass_of( 'WordPress\\XML\\XMLProcessor', 'WordPress\\XML\\NativeXMLProcessor' ) );
-PHP
- To force the safe pure-PHP path for comparison, define WP_NATIVE_APIS_DISABLE_DEFAULTS before loading the toolkit:
- php -d extension=extensions/native-apis/target/release/libwp_native_apis.so <<'PHP'
-<?php
-define( 'WP_NATIVE_APIS_DISABLE_DEFAULTS', true );
-require __DIR__ . '/bootstrap.php';
-
-var_dump( is_subclass_of( 'WP_HTML_Tag_Processor', 'WP_HTML_Native_Tag_Processor' ) );
-PHP
-
-
-
- Run the benchmark
- The same benchmark command runs in CI for the release page. It requires the native classes, compares PHP and native modes, and disables native defaults so both paths are measured deliberately.
- php -d extension=extensions/native-apis/target/release/libwp_native_apis.so \
- bin/benchmark-native-apis.php \
- --iterations=50 \
- --mode=both \
- --disable-native-defaults \
- --require-native
- Benchmark numbers are machine-specific. Use them to compare PHP and native behavior on the same machine, not as universal throughput guarantees.
-
-
-
- Troubleshooting
-
-
- php-config was not found
- Install PHP development headers for the PHP version you want to target, or run the build with PHP_CONFIG=/path/to/php-config.
-
-
- PHP cannot load the shared object
- Confirm the shared object path is correct and that the runtime PHP binary matches the PHP ABI used for compilation.
-
-
- Clang or libclang is missing
- Install Clang and libclang, or set LIBCLANG_PATH to the directory containing the libclang library.
-
-
- You only want to test Rust parser kernels
- Run cd extensions/native-apis && cargo test. This path does not require PHP development headers.
-
-
-
-
-
-
-
-
-
diff --git a/docs/native-rust-apis.md b/docs/native-rust-apis.md
deleted file mode 100644
index 1c019facf..000000000
--- a/docs/native-rust-apis.md
+++ /dev/null
@@ -1,394 +0,0 @@
-# Native Rust API Extension Notes
-
-This document captures the first-pass implementation shape for Rust-backed PHP
-extensions for the HTML, XML, and URL-in-text APIs.
-
-## Reference Architecture
-
-The local reference checkout is:
-
-```text
-/home/claude/explore-sqlite-rust/packages/php-ext-wp-mysql-parser
-```
-
-That package builds a PHP extension with `ext-php-rs`:
-
-```bash
-PHP_CONFIG=/path/to/php-config \
-LIBCLANG_PATH=/path/to/libclang/lib \
-cargo build --release
-```
-
-The extension registers native classes such as `WP_MySQL_Native_Lexer` and
-`WP_MySQL_Native_Parser`. The PHP package keeps public classes stable:
-
-- PHP load code checks `class_exists( 'WP_MySQL_Native_Lexer', false )`.
-- If native classes exist, public wrapper classes are loaded from a `native/`
- directory.
-- If they do not exist, pure-PHP classes are loaded.
-- The public parser class still extends the pure-PHP base class where caller
- `instanceof` contracts matter, but delegates work to a composed native object.
-- Native-only tests are skipped when native classes are unavailable.
-- Verification scripts fail with explicit stderr diagnostics when the extension
- is missing or not wired into the public API.
-
-The same pattern should be used here: first add native classes and shared
-conformance tests, then stack PHP default-to-native wrappers after correctness is
-proven.
-
-## Current Repository Surface
-
-- `components/HTML` contains the global `WP_HTML_Tag_Processor` and
- `WP_HTML_Processor` APIs.
-- `components/XML` contains `WordPress\XML\XMLProcessor` and W3C-oriented tests.
-- `components/DataLiberation/URL` contains a WHATWG URL parser wrapper and
- `URLInTextProcessor` for plain-text URL detection. This native extension stack
- does not add an RFC URL parser; it adds a native URL-in-text candidate scanner
- that the public processor can validate through the existing WHATWG parser.
-- The root autoloader is classmap-based. Do not convert this to PSR-4.
-
-## Proposed Native Package Layout
-
-Start the first native branch with a reviewable package that does not change the
-default PHP behavior:
-
-```text
-extensions/native-apis/
- Cargo.toml
- README.md
- src/
- lib.rs
- html.rs
- xml.rs
- tests/
- verify-native-apis.php
-```
-
-Native class names should avoid taking over public names until the second stacked
-branch:
-
-- `WP_HTML_Native_Tag_Processor`
-- `WP_HTML_Native_Processor`
-- `WordPress\XML\NativeXMLProcessor`
-- `WordPress\DataLiberation\URL\NativeURLInTextProcessor`
-The current stack includes PHP wrappers that select native implementations when
-the classes are already registered by the extension, while preserving PHP-only
-fallback behavior.
-
-## Native Default Controls
-
-Public wrappers use native delegates only when the extension has already loaded
-the matching native classes. Define `WP_NATIVE_APIS_DISABLE_DEFAULTS` as a
-truthy constant before loading the components to force every public wrapper back
-to the PHP implementation.
-
-`WP_HTML_Processor::create_full_parser()` remains PHP-backed for now even when
-the native extension is loaded, because the native full-document parser is not
-yet public-parity safe. `WP_HTML_Processor::create_fragment()` can use native
-delegates for covered table, list, description-list, select/option/optgroup,
-omitted-paragraph, and ruby tree-builder cases.
-
-`URLInTextProcessor` can use the native URL-in-text scanner as its thick sieve
-for ASCII input when enabled. The public class still validates every candidate
-with the existing WHATWG parser and falls back to the PHP regular-expression
-sieve for non-ASCII text or when the native scanner is unavailable.
-
-## Shared Conformance Strategy
-
-Conformance tests should be implementation-parameterized, not duplicated. Each
-suite should expose a provider with at least:
-
-- pure PHP class factory
-- native class factory, skipped when the native class is unavailable
-
-HTML tests should focus first on the public streaming surface:
-
-- `next_token()`
-- `next_tag()`
-- `get_token_type()`
-- `get_token_name()`
-- `get_tag()`
-- `get_namespace()` for the current first-slice HTML namespace
-- `get_qualified_tag_name()` and `get_qualified_attribute_name()` for the
- current first-slice HTML namespace
-- `get_attribute()`
-- `class_list()` and `has_class()` for decoded class access
-- `get_attribute_names_with_prefix()` for prefixed attribute-name scans
-- `remove_attribute()` for current-token attribute removal bookkeeping
-- `is_virtual()` for current-token virtual status
-- `get_last_error()` diagnostics
-- `get_unsupported_exception()` diagnostics
-- `expects_closer()` for current-token closer expectation
-- `is_void()` static void-element checks
-- `is_special()` static special-category checks
-- `has_self_closing_flag()` syntax checks
-- `paused_at_incomplete_token()` complete-input status for direct native tag
- processors
-- `next_tag_summary_batch()` public row batches for direct native tag
- processors, including current-tag state after native batch advancement
-- `next_tag_prefix_count_compact_batch()` current-tag state after public
- native-default count-only batch advancement
-- `next_matching_tag_summary_batch()` public row batches for direct native tag
- processors
-- `next_matching_tag_attribute_summary_batch()` public row batches for direct
- native tag processors
-- `next_matching_tag_attributes_summary_batch()` public row batches for direct
- native tag processors
-- `get_modifiable_text()` for text, comment, RAWTEXT, and RCDATA tokens
-- public native-default `set_modifiable_text()` and
- `subdivide_text_appropriately()` delegation after native token advancement
-- public native-default remaining-document aggregate scans leaving the public
- tag processor in the same complete state as PHP fallback scans
-- `get_breadcrumbs()`
-- `matches_breadcrumbs()` suffix and wildcard checks
-- `set_bookmark()`, `seek()`, `has_bookmark()`, and `release_bookmark()` for
- direct native cursor state restore
-- serialization behavior for `WP_HTML_Processor`
- when native defaults are loaded, including the PHP serializer fallback used
- by public `normalize()` / `serialize()` and the started-processor rejection
- after native token or token-summary advancement
-
-XML tests should focus first on:
-
-- `create_from_string()`
-- `next_token()`
-- `next_tag()`
-- namespace-aware tag and attribute reads
-- malformed document diagnostics via `get_last_error()`
-- complete-input status, including rejected `append_bytes()` calls on direct
- native complete-string processors
-- public native-default structural, metadata, payload, content, and import
- inventory summaries leaving the processor finished after remaining-document
- native scans
-- public native-default token, tag, matching-tag, prefixed-attribute, and
- document-removal aggregate summaries leaving the processor finished after
- remaining-document native scans
-- public native-default token, tag, matching-tag, and count batches leaving the
- processor finished after exhausted native batch scans
-- `next_token_summary_batch()` public row batches for direct native processors
-- `next_tag_summary_batch()` public row batches for direct native processors
-- `next_matching_tag_summary_batch()` public row batches for direct native
- processors
-- `set_bookmark()`, `seek()`, `has_bookmark()`, and `release_bookmark()` for
- direct native cursor state restore
-
-The native XML first slice now resolves namespace declarations into
-`get_tag_local_name()`, `get_tag_namespace()`, and
-`get_tag_namespace_and_local_name()` results, and stores namespaced attributes
-under the resolved `{namespace}local_name` key for conformance and verification
-coverage.
-
-Native-specific tests should cover wrapper identity, skipped native availability,
-and diagnostics, following the sqlite parser extension tests.
-
-URL-in-text tests should cover:
-
-- direct native candidate scanning for HTTP, HTTPS, protocol-relative, and bare
- domain references
-- trailing punctuation preservation during replacements
-- malformed host and malformed port rejection/truncation before WHATWG
- validation
-- public `URLInTextProcessor` behavior with and without the native extension
- loaded, proving the existing WHATWG parser remains the fine sieve
-
-## Benchmarking
-
-Use `bin/benchmark-native-apis.php` to capture PHP baseline CPU and memory
-before native work starts. Re-run the same command with the extension loaded:
-
-```bash
-php bin/benchmark-native-apis.php --iterations=50
-php -d extension=/path/to/libwp_native_apis.so \
- bin/benchmark-native-apis.php --iterations=50 --mode=both --require-native
-```
-
-The benchmark supports `--mode=php|native|both` and emits an `implementation`
-field in each result row. Native rows fail softly with unavailable-class
-diagnostics when the extension is not loaded, so CI or local workers can still
-verify the harness without PHP development headers.
-
-Pass `--disable-native-defaults` when the extension is loaded but the PHP row
-should force public wrappers back to PHP fallback classes. The harness defines
-`WP_NATIVE_APIS_DISABLE_DEFAULTS` before loading the repository bootstrap so the
-global native-default kill switch applies consistently across HTML, XML, and
-URL workloads.
-
-Use `--require-native` in post-build benchmark jobs. It preserves the soft
-missing-extension behavior for PHP-only development runs, but exits non-zero if
-any selected native row is unavailable.
-
-Pass `--component=url --name=url-in-text-processor` to benchmark URL-in-text
-scans specifically. The PHP row measures the public `URLInTextProcessor`, while
-the native row measures the direct native scanner.
-
-Record the PHP version, command, wall time, CPU time, and peak memory in
-`.autonomous-loop/memory.md` after each run.
-
-## Performance API Shape
-
-The native defaults preserve the existing public token and tag processors, but
-token-by-token loops still execute PHP code for every public method call. The
-performance branch therefore adds explicit fused and chunked APIs for common
-high-throughput workflows where callers can accept summarized rows instead of
-calling several accessors per token or tag.
-
-HTML tag workflows now have compact chunked summaries for incremental callers
-that only need tag names and closer state:
-
-- `next_tag_summary_batch()` returns structured rows for the next chunk of
- tag metadata, including direct native public-row batches.
-- `next_tag_compact_summary_batch()` returns the same rows as a compact string.
-- `next_matching_tag_summary_batch()` returns structured rows for matching
- tags, including direct native public-row batches.
-- `next_matching_tag_attribute_summary_batch()` returns structured rows for
- matching tags and one decoded attribute value, including direct native
- public-row batches.
-- `next_matching_tag_attributes_summary_batch()` returns structured rows for
- matching tags and decoded multi-attribute maps, including direct native
- public-row batches.
-- `summarize_tag_inventory()` consumes the remaining tag stream and returns
- tag, opener, closer, attribute, and unique tag-name counts for audit
- workflows that do not need one PHP call per tag.
-- `summarize_heading_inventory()` consumes the remaining tag stream and returns
- heading counts by level for outline audits that do not need one PHP call per
- tag.
-- `summarize_id_inventory()` consumes the remaining tag stream and returns
- ID-bearing tag counts, unique decoded ID counts, duplicate ID counts, and
- decoded ID value bytes for anchor/accessibility audits.
-- `summarize_attribute_inventory()` consumes the remaining tag stream and
- returns attribute, unique attribute-name, and decoded attribute-value byte
- counts for audits that do not need one PHP call per tag or attribute.
-- `summarize_data_attribute_inventory()` consumes the remaining tag stream and
- returns `data-*` tag, attribute, unique-name, and decoded value-byte counts
- for custom-data audits.
-- `summarize_aria_attribute_inventory()` consumes the remaining tag stream and
- returns `aria-*` tag, attribute, unique-name, and decoded value-byte counts
- for accessibility audits.
-- `summarize_class_inventory()` consumes the remaining tag stream and returns
- class-attribute, per-tag class-name, unique class-name, and decoded class
- value byte counts for class audits that do not need one PHP call per tag.
-- `summarize_resource_inventory()` consumes the remaining tag stream and
- returns counts and decoded value bytes for common `href`/`src` resources on
- anchors, images, scripts, links, and sources.
-- `summarize_image_inventory()` consumes the remaining tag stream and returns
- image, `src`, `alt`, empty-alt, dimension, and decoded value-byte counts for
- image audits.
-- `summarize_form_inventory()` consumes the remaining tag stream and returns
- form/control counts, named-control counts, unique control-name counts, and
- decoded control-name byte totals.
-
-HTML tag-prefix workflows build on that pattern with three shapes:
-
-- `count_attribute_names_with_prefix()` counts matching attributes on the
- current tag.
-- `summarize_attribute_names_with_prefix()` and
- `remove_attributes_with_prefix_from_document()` consume the remaining
- document in one call for fully fused read-only or sanitizer workflows.
-- `next_tag_prefix_summary_batch()` and
- `next_tag_prefix_compact_summary_batch()` consume the next chunk of tags,
- preserving incremental processing while amortizing PHP/native crossings.
-- `next_tag_prefix_count_compact_batch()` consumes the next chunk of tags and
- returns only aggregate tag and prefixed-attribute counts for callers that do
- not need per-tag rows. Native-backed public wrappers preserve the current tag
- after count-only batch advancement and map final short batches to the same
- complete public parser state as the PHP fallback loop.
-
-HTML processor token workflows now also expose `next_token_summary_batch()` and
-`next_token_compact_summary_batch()` for common read-only scans that need token
-type, token name, closer state, depth, and breadcrumbs without several accessor
-calls per token. Native-backed public wrappers map full remaining-document and
-final short token batches to the same complete public parser state as the PHP
-fallback loop.
-
-XML token workflows follow the same pattern:
-
-- Direct native XML processor instances expose `create_for_streaming()` and
- `get_reentrancy_cursor()` for the supported UTF-8 native-cursor factory path.
-- `summarize_token_stream()` consumes the remaining token stream and returns
- aggregate token, tag, and attribute counts.
-- `summarize_document_inventory()` consumes the remaining token stream and
- returns structural counts such as open/closing tags, text/comment/CDATA
- tokens, maximum depth, and empty elements.
-- `summarize_attribute_inventory()` consumes the remaining token stream and
- returns opening-tag attribute counts, namespaced attribute counts, tags with
- attributes, and maximum attributes per tag.
-- `summarize_id_inventory()` consumes the remaining token stream and returns
- no-namespace `id` attribute counts, unique ID counts, duplicate ID counts,
- and decoded ID value bytes for importer/dedupe audits.
-- `summarize_content_inventory()` consumes the remaining token stream and
- returns combined attribute-value and payload-byte counts for importer/audit
- workflows that need both metadata and text-like content in one pass.
-- `summarize_import_inventory()` consumes the remaining token stream and
- returns combined structural, attribute, and payload counts for importer/audit
- workflows that need one document-shape and content-size pass.
-- `next_token_summary_batch()` returns structured rows for the next chunk of
- token metadata.
-- `next_token_compact_summary_batch()` returns the same rows as a compact
- string using `\x1f` field separators and `\x1e` record separators.
-
-XML tag workflows also expose `next_tag_summary_batch()` and
-`next_tag_compact_summary_batch()` for incremental read-only scans that only
-need opening tag metadata and one cached attribute. The compact rows include
-the number of tokens consumed in the current batch so native-backed public
-wrappers can replay accurately if a later call falls back to PHP.
-
-Count-only XML tag workflows can use `next_tag_count_batch()` for structured
-counts or `next_tag_count_compact_batch()` for a compact `token_count`,
-`tag_count`, and `attribute_count` row. This caller shape avoids per-tag
-metadata rows when the caller only needs aggregate progress through an
-incremental scan.
-
-Namespace/local-name XML tag scans can use `next_matching_tag_summary_batch()`,
-`next_matching_tag_compact_summary_batch()`, `next_matching_tag_count_batch()`,
-`next_matching_tag_count_compact_batch()`, or `summarize_matching_tag_stream()`.
-These match the common `next_tag( array( $namespace, $local_name ) )` loop shape
-while allowing native implementations to skip unrelated tags and amortize or
-avoid the crossing cost.
-
-HTML tag-name scans that also read one attribute can use
-`next_matching_tag_attribute_summary_batch()` or
-`next_matching_tag_attribute_compact_summary_batch()`. This matches common link,
-image, and script scans where the caller filters by tag name and immediately
-extracts `href`, `src`, or a similar single attribute.
-
-HTML tag-name scans that need several attributes can use
-`next_matching_tag_attributes_summary_batch()` or
-`next_matching_tag_attributes_compact_summary_batch()`. This matches link-audit
-workflows that filter to `A` tags and extract fields such as `href`, `title`,
-and `rel` in one native scan instead of one crossing per attribute.
-
-HTML link-audit workflows that only need aggregate counts can use
-`summarize_matching_tag_attributes()`. This consumes the remaining matching tags
-in one call and returns tag, present-attribute, and decoded attribute byte
-counts without PHP parsing one compact row per link.
-
-The structured array batch methods are the readability path. The compact string
-methods are the performance path for callers that can aggregate directly from
-the separator-delimited rows.
-
-Current benchmark interpretation: native-backed public wrappers clear the 5x
-CPU target with the same peak memory for caller-shaped HTML/XML fused or chunked
-workflows. This includes HTML sanitizer, processor, token batch, compact tag,
-matching-tag, matching-attribute, prefix, and inventory rows; and XML fused
-token/tag/prefix summaries, compact token/tag/count batches, matching-tag
-batches, sanitizer rows, and document, attribute, ID, namespace, payload,
-structural, content, and import inventory summaries.
-
-Direct native HTML processor instances also expose qualified-name accessors,
-complete-input pause status, prefixed-attribute count and aggregate summary
-access, compact tag-prefix count batches, inventory aggregate summaries,
-current-token and document-level prefixed-attribute removal,
-normalization/serialization through the PHP fragment serializer, text
-subdivision, modifiable text mutation, DOCTYPE info access, full-parser
-factory, processor stepping, namespace switching, attribute setting, class
-additions/removals, updated HTML serialization and string-cast serialization,
-structured and compact token/tag/matching-tag summary rows, and matching-tag
-attribute aggregate summaries for parity with the public processor API.
-
-The unqualified performance target remains open for transparent generic public
-cursor loops. Focused generic HTML cursor snapshots improved substantially, but
-the reviewable performance claim is still scoped to caller-shaped rows. Generic
-public `next_tag()` and `next_token()` loops that call several accessors per
-item, especially the XML `next_token()` cursor row, remain compatibility paths
-below 5x because they still pay repeated PHP method dispatch plus native row
-hydration or accessor export overhead.
diff --git a/docs/reference/index.html b/docs/reference/index.html
index 6579c9a23..caff1c641 100644
--- a/docs/reference/index.html
+++ b/docs/reference/index.html
@@ -5,7 +5,7 @@
Reference — PHP Toolkit
-
+
@@ -66,12 +65,6 @@ WordPress runtime support
-
-
diff --git a/extensions/native-apis/.gitignore b/extensions/native-apis/.gitignore
deleted file mode 100644
index b83d22266..000000000
--- a/extensions/native-apis/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/target/
diff --git a/extensions/native-apis/Cargo.lock b/extensions/native-apis/Cargo.lock
deleted file mode 100644
index d1f2a76a7..000000000
--- a/extensions/native-apis/Cargo.lock
+++ /dev/null
@@ -1,1683 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "adler2"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
-
-[[package]]
-name = "aes"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8"
-dependencies = [
- "cipher",
- "cpubits",
- "cpufeatures 0.3.0",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "1.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "anyhow"
-version = "1.0.102"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
-
-[[package]]
-name = "base64"
-version = "0.22.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
-
-[[package]]
-name = "base64ct"
-version = "1.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
-
-[[package]]
-name = "bitflags"
-version = "2.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
-
-[[package]]
-name = "block-buffer"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "block-buffer"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
-dependencies = [
- "hybrid-array",
- "zeroize",
-]
-
-[[package]]
-name = "bumpalo"
-version = "3.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
-
-[[package]]
-name = "bytecount"
-version = "0.6.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
-
-[[package]]
-name = "bytes"
-version = "1.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
-
-[[package]]
-name = "bzip2"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
-dependencies = [
- "libbz2-rs-sys",
-]
-
-[[package]]
-name = "camino"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
-dependencies = [
- "serde_core",
-]
-
-[[package]]
-name = "cargo-platform"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "cargo_metadata"
-version = "0.14.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa"
-dependencies = [
- "camino",
- "cargo-platform",
- "semver",
- "serde",
- "serde_json",
-]
-
-[[package]]
-name = "cc"
-version = "1.2.62"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
-dependencies = [
- "find-msvc-tools",
- "jobserver",
- "libc",
- "shlex",
-]
-
-[[package]]
-name = "cexpr"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
-dependencies = [
- "nom",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "cipher"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea"
-dependencies = [
- "crypto-common 0.2.1",
- "inout",
-]
-
-[[package]]
-name = "cmov"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
-
-[[package]]
-name = "const-oid"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
-
-[[package]]
-name = "constant_time_eq"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
-
-[[package]]
-name = "convert_case"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
-dependencies = [
- "unicode-segmentation",
-]
-
-[[package]]
-name = "core-foundation"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-
-[[package]]
-name = "cpubits"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
-
-[[package]]
-name = "cpufeatures"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "cpufeatures"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crc32fast"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
-dependencies = [
- "generic-array",
- "typenum",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "ctutils"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
-dependencies = [
- "cmov",
-]
-
-[[package]]
-name = "darling"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
-dependencies = [
- "darling_core",
- "darling_macro",
-]
-
-[[package]]
-name = "darling_core"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
-dependencies = [
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim",
- "syn",
-]
-
-[[package]]
-name = "darling_macro"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
-dependencies = [
- "darling_core",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "deflate64"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
-
-[[package]]
-name = "der"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b"
-dependencies = [
- "pem-rfc7468",
- "zeroize",
-]
-
-[[package]]
-name = "deranged"
-version = "0.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
-dependencies = [
- "powerfmt",
-]
-
-[[package]]
-name = "digest"
-version = "0.10.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
-dependencies = [
- "block-buffer 0.10.4",
- "crypto-common 0.1.7",
-]
-
-[[package]]
-name = "digest"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
-dependencies = [
- "block-buffer 0.12.0",
- "const-oid",
- "crypto-common 0.2.1",
- "ctutils",
- "zeroize",
-]
-
-[[package]]
-name = "either"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
-
-[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-
-[[package]]
-name = "errno"
-version = "0.3.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
-dependencies = [
- "libc",
- "windows-sys",
-]
-
-[[package]]
-name = "error-chain"
-version = "0.12.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
-dependencies = [
- "version_check",
-]
-
-[[package]]
-name = "ext-php-rs"
-version = "0.15.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89ba219f32e80a5ab8b2562b873f15aeac8028aff1fb1a898409b2e251bfe59d"
-dependencies = [
- "anyhow",
- "bitflags",
- "cc",
- "cfg-if",
- "ext-php-rs-bindgen",
- "ext-php-rs-build",
- "ext-php-rs-derive",
- "inventory",
- "native-tls",
- "once_cell",
- "parking_lot",
- "skeptic",
- "ureq",
- "zip",
-]
-
-[[package]]
-name = "ext-php-rs-bindgen"
-version = "0.72.1-extphprs.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4795dd0976bd7d7d321c49e88e836f8e5b5b2b481e089067e303f2945617458a"
-dependencies = [
- "bitflags",
- "cexpr",
- "ext-php-rs-clang-sys",
- "itertools",
- "log",
- "prettyplease",
- "proc-macro2",
- "quote",
- "regex",
- "rustc-hash",
- "shlex",
- "syn",
-]
-
-[[package]]
-name = "ext-php-rs-build"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "561bce8a6312a6182c078cd987d8d9cb6bf9292a35817cfd009ea0bffa794f5f"
-dependencies = [
- "anyhow",
-]
-
-[[package]]
-name = "ext-php-rs-clang-sys"
-version = "1.8.1-extphprs.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa1ad6e482017d457d57d73691f8bed148a8a6198babe90830310c3308480a61"
-dependencies = [
- "glob",
- "libc",
- "libloading",
-]
-
-[[package]]
-name = "ext-php-rs-derive"
-version = "0.11.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6cb94d5f4c1e9758b6b936ad306dea36a034c125334d9a438fe966a5d596a85"
-dependencies = [
- "convert_case",
- "darling",
- "itertools",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "fastrand"
-version = "2.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
-
-[[package]]
-name = "find-msvc-tools"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
-
-[[package]]
-name = "flate2"
-version = "1.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
-dependencies = [
- "crc32fast",
- "miniz_oxide",
- "zlib-rs",
-]
-
-[[package]]
-name = "foldhash"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-
-[[package]]
-name = "foreign-types"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
-
-[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
-
-[[package]]
-name = "futures-core"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
-
-[[package]]
-name = "futures-task"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
-
-[[package]]
-name = "futures-util"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
-dependencies = [
- "futures-core",
- "futures-task",
- "pin-project-lite",
- "slab",
-]
-
-[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
-dependencies = [
- "cfg-if",
- "libc",
- "r-efi 5.3.0",
- "wasip2",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
-dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "r-efi 6.0.0",
- "wasip2",
- "wasip3",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "glob"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
-
-[[package]]
-name = "hashbrown"
-version = "0.15.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
-dependencies = [
- "foldhash",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
-
-[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
-[[package]]
-name = "hmac"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
-dependencies = [
- "digest 0.11.3",
-]
-
-[[package]]
-name = "http"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
-dependencies = [
- "bytes",
- "itoa",
-]
-
-[[package]]
-name = "httparse"
-version = "1.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
-
-[[package]]
-name = "hybrid-array"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
-dependencies = [
- "typenum",
-]
-
-[[package]]
-name = "id-arena"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
-
-[[package]]
-name = "ident_case"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
-
-[[package]]
-name = "indexmap"
-version = "2.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
-dependencies = [
- "equivalent",
- "hashbrown 0.17.1",
- "serde",
- "serde_core",
-]
-
-[[package]]
-name = "inout"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "inventory"
-version = "0.3.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
-dependencies = [
- "rustversion",
-]
-
-[[package]]
-name = "itertools"
-version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
-dependencies = [
- "either",
-]
-
-[[package]]
-name = "itoa"
-version = "1.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
-
-[[package]]
-name = "jobserver"
-version = "0.1.34"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
-dependencies = [
- "getrandom 0.3.4",
- "libc",
-]
-
-[[package]]
-name = "js-sys"
-version = "0.3.98"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
-dependencies = [
- "cfg-if",
- "futures-util",
- "once_cell",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "leb128fmt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
-
-[[package]]
-name = "libbz2-rs-sys"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8fc329e1457d97a9d58a4e2ca49e3be572431a7e096008efc2e3a3c19d428f4"
-
-[[package]]
-name = "libc"
-version = "0.2.186"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
-
-[[package]]
-name = "libloading"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
-dependencies = [
- "cfg-if",
- "windows-link",
-]
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
-
-[[package]]
-name = "lock_api"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
-
-[[package]]
-name = "log"
-version = "0.4.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
-
-[[package]]
-name = "lzma-rust2"
-version = "0.16.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae"
-dependencies = [
- "sha2",
-]
-
-[[package]]
-name = "memchr"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
-
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
-name = "miniz_oxide"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
-dependencies = [
- "adler2",
- "simd-adler32",
-]
-
-[[package]]
-name = "native-tls"
-version = "0.2.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
-dependencies = [
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework",
- "security-framework-sys",
- "tempfile",
-]
-
-[[package]]
-name = "nom"
-version = "7.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
-dependencies = [
- "memchr",
- "minimal-lexical",
-]
-
-[[package]]
-name = "num-conv"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
-
-[[package]]
-name = "once_cell"
-version = "1.21.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
-
-[[package]]
-name = "openssl"
-version = "0.10.79"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
-dependencies = [
- "bitflags",
- "cfg-if",
- "foreign-types",
- "libc",
- "openssl-macros",
- "openssl-sys",
-]
-
-[[package]]
-name = "openssl-macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "openssl-probe"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
-
-[[package]]
-name = "openssl-sys"
-version = "0.9.115"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
-]
-
-[[package]]
-name = "pbkdf2"
-version = "0.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
-dependencies = [
- "digest 0.11.3",
- "hmac",
-]
-
-[[package]]
-name = "pem-rfc7468"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
-dependencies = [
- "base64ct",
-]
-
-[[package]]
-name = "percent-encoding"
-version = "2.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
-
-[[package]]
-name = "pkg-config"
-version = "0.3.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
-
-[[package]]
-name = "powerfmt"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
-
-[[package]]
-name = "ppmd-rust"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
-
-[[package]]
-name = "prettyplease"
-version = "0.2.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
-dependencies = [
- "proc-macro2",
- "syn",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "pulldown-cmark"
-version = "0.9.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
-dependencies = [
- "bitflags",
- "memchr",
- "unicase",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "r-efi"
-version = "5.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
-
-[[package]]
-name = "r-efi"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
-
-[[package]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "regex"
-version = "1.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
-
-[[package]]
-name = "rustc-hash"
-version = "2.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
-
-[[package]]
-name = "rustix"
-version = "1.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys",
-]
-
-[[package]]
-name = "rustls-pki-types"
-version = "1.14.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
-dependencies = [
- "zeroize",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
-
-[[package]]
-name = "same-file"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
-name = "schannel"
-version = "0.1.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
-dependencies = [
- "windows-sys",
-]
-
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
-name = "security-framework"
-version = "3.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
-dependencies = [
- "bitflags",
- "core-foundation",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework-sys"
-version = "2.17.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
-[[package]]
-name = "semver"
-version = "1.0.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
-dependencies = [
- "serde",
- "serde_core",
-]
-
-[[package]]
-name = "serde"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
-dependencies = [
- "serde_core",
- "serde_derive",
-]
-
-[[package]]
-name = "serde_core"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "serde_json"
-version = "1.0.149"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
-dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
-]
-
-[[package]]
-name = "sha1"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.3.0",
- "digest 0.11.3",
-]
-
-[[package]]
-name = "sha2"
-version = "0.10.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.2.17",
- "digest 0.10.7",
-]
-
-[[package]]
-name = "shlex"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-[[package]]
-name = "simd-adler32"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
-
-[[package]]
-name = "skeptic"
-version = "0.13.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8"
-dependencies = [
- "bytecount",
- "cargo_metadata",
- "error-chain",
- "glob",
- "pulldown-cmark",
- "tempfile",
- "walkdir",
-]
-
-[[package]]
-name = "slab"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
-
-[[package]]
-name = "smallvec"
-version = "1.15.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
-
-[[package]]
-name = "strsim"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
-
-[[package]]
-name = "syn"
-version = "2.0.117"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "tempfile"
-version = "3.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
-dependencies = [
- "fastrand",
- "getrandom 0.4.2",
- "once_cell",
- "rustix",
- "windows-sys",
-]
-
-[[package]]
-name = "time"
-version = "0.3.47"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
-dependencies = [
- "deranged",
- "js-sys",
- "num-conv",
- "powerfmt",
- "serde_core",
- "time-core",
-]
-
-[[package]]
-name = "time-core"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
-
-[[package]]
-name = "typed-path"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
-
-[[package]]
-name = "typenum"
-version = "1.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
-
-[[package]]
-name = "unicase"
-version = "2.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
-
-[[package]]
-name = "unicode-xid"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
-
-[[package]]
-name = "ureq"
-version = "3.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
-dependencies = [
- "base64",
- "der",
- "flate2",
- "log",
- "native-tls",
- "percent-encoding",
- "rustls-pki-types",
- "ureq-proto",
- "utf8-zero",
- "webpki-root-certs",
-]
-
-[[package]]
-name = "ureq-proto"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
-dependencies = [
- "base64",
- "http",
- "httparse",
- "log",
-]
-
-[[package]]
-name = "utf8-zero"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
-
-[[package]]
-name = "vcpkg"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-
-[[package]]
-name = "version_check"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
-
-[[package]]
-name = "walkdir"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
-dependencies = [
- "same-file",
- "winapi-util",
-]
-
-[[package]]
-name = "wasip2"
-version = "1.0.3+wasi-0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
-dependencies = [
- "wit-bindgen 0.57.1",
-]
-
-[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
-dependencies = [
- "wit-bindgen 0.51.0",
-]
-
-[[package]]
-name = "wasm-bindgen"
-version = "0.2.121"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
-dependencies = [
- "cfg-if",
- "once_cell",
- "rustversion",
- "wasm-bindgen-macro",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.121"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.121"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
-dependencies = [
- "bumpalo",
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.121"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
-dependencies = [
- "leb128fmt",
- "wasmparser",
-]
-
-[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
-dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser",
-]
-
-[[package]]
-name = "wasmparser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
-dependencies = [
- "bitflags",
- "hashbrown 0.15.5",
- "indexmap",
- "semver",
-]
-
-[[package]]
-name = "webpki-root-certs"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
-dependencies = [
- "rustls-pki-types",
-]
-
-[[package]]
-name = "winapi-util"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
-dependencies = [
- "windows-sys",
-]
-
-[[package]]
-name = "windows-link"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
-
-[[package]]
-name = "windows-sys"
-version = "0.61.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "wit-bindgen"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
-dependencies = [
- "wit-bindgen-rust-macro",
-]
-
-[[package]]
-name = "wit-bindgen"
-version = "0.57.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
-
-[[package]]
-name = "wit-bindgen-core"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
-dependencies = [
- "anyhow",
- "heck",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-bindgen-rust"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
-dependencies = [
- "anyhow",
- "heck",
- "indexmap",
- "prettyplease",
- "syn",
- "wasm-metadata",
- "wit-bindgen-core",
- "wit-component",
-]
-
-[[package]]
-name = "wit-bindgen-rust-macro"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
-dependencies = [
- "anyhow",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn",
- "wit-bindgen-core",
- "wit-bindgen-rust",
-]
-
-[[package]]
-name = "wit-component"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
-dependencies = [
- "anyhow",
- "bitflags",
- "indexmap",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder",
- "wasm-metadata",
- "wasmparser",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-parser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser",
-]
-
-[[package]]
-name = "wp_native_apis"
-version = "0.1.0"
-dependencies = [
- "ext-php-rs",
-]
-
-[[package]]
-name = "zeroize"
-version = "1.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
-
-[[package]]
-name = "zip"
-version = "8.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
-dependencies = [
- "aes",
- "bzip2",
- "constant_time_eq",
- "crc32fast",
- "deflate64",
- "flate2",
- "getrandom 0.4.2",
- "hmac",
- "indexmap",
- "lzma-rust2",
- "memchr",
- "pbkdf2",
- "ppmd-rust",
- "sha1",
- "time",
- "typed-path",
- "zeroize",
- "zopfli",
- "zstd",
-]
-
-[[package]]
-name = "zlib-rs"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
-
-[[package]]
-name = "zmij"
-version = "1.0.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
-
-[[package]]
-name = "zopfli"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
-dependencies = [
- "bumpalo",
- "crc32fast",
- "log",
- "simd-adler32",
-]
-
-[[package]]
-name = "zstd"
-version = "0.13.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
-dependencies = [
- "zstd-safe",
-]
-
-[[package]]
-name = "zstd-safe"
-version = "7.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
-dependencies = [
- "zstd-sys",
-]
-
-[[package]]
-name = "zstd-sys"
-version = "2.0.16+zstd.1.5.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
-dependencies = [
- "cc",
- "pkg-config",
-]
diff --git a/extensions/native-apis/Cargo.toml b/extensions/native-apis/Cargo.toml
deleted file mode 100644
index af82f472d..000000000
--- a/extensions/native-apis/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[package]
-name = "wp_native_apis"
-version = "0.1.0"
-edition = "2021"
-license = "GPL-2.0-or-later"
-publish = false
-
-[lib]
-crate-type = ["cdylib", "rlib", "staticlib"]
-
-[features]
-default = []
-php-extension = ["dep:ext-php-rs"]
-
-[dependencies]
-ext-php-rs = { version = "=0.15.13", default-features = false, features = ["runtime"], optional = true }
-
-[profile.release]
-lto = "thin"
-codegen-units = 1
diff --git a/extensions/native-apis/README.md b/extensions/native-apis/README.md
deleted file mode 100644
index 19bbbe094..000000000
--- a/extensions/native-apis/README.md
+++ /dev/null
@@ -1,455 +0,0 @@
-# WordPress Native APIs PHP Extension
-
-This package is the first Rust-backed PHP extension surface for the toolkit's
-native HTML, XML, and URL-in-text API work. It registers native classes that the public
-PHP wrappers can use when the extension is loaded, while preserving PHP-only
-fallback behavior when it is unavailable or explicitly disabled.
-
-Public docs and releases:
-
-- User-facing overview:
-- Playground release index:
-- Latest Playground smoke test:
-
-## Native Classes
-
-- `WP_HTML_Native_Tag_Processor`
-- `WP_HTML_Native_Processor`
-- `WordPress\XML\NativeXMLProcessor`
-- `WordPress\DataLiberation\URL\NativeURLInTextProcessor`
-
-## Quick Setup
-
-Use this path when you want to build, load, and verify the extension on a
-development machine.
-
-You need:
-
-- PHP CLI with matching development headers and `php-config`.
-- Rust and Cargo.
-- Clang and libclang for `bindgen`.
-- Composer dependencies from the repository root.
-
-On Ubuntu, the GitHub Actions job uses:
-
-```bash
-sudo apt-get update
-sudo apt-get install -y clang libclang-dev
-composer install --prefer-dist --no-progress --no-suggest
-```
-
-Build and verify from the repository root:
-
-```bash
-extensions/native-apis/build-extension.sh
-
-php -d extension=extensions/native-apis/target/release/libwp_native_apis.so \
- extensions/native-apis/tests/verify-native-apis.php
-```
-
-If `php-config` is not on `PATH`, pass it explicitly:
-
-```bash
-PHP_CONFIG=/path/to/php-config \
-LIBCLANG_PATH=/path/to/libclang/lib \
-extensions/native-apis/build-extension.sh
-```
-
-Check the Rust parser kernels without PHP development headers:
-
-```bash
-cd extensions/native-apis
-cargo test
-```
-
-Check that public PHP classes progressively resolve to native classes by loading
-the extension before the repository bootstrap:
-
-```bash
-php -d extension=extensions/native-apis/target/release/libwp_native_apis.so <<'PHP'
-/manifest.json
-```
-
-Use `latest` when manually testing the newest build. Use an immutable
-`` manifest when documenting a reproducible Playground URL, writing
-tests, or comparing behavior across releases.
-
-The release index page lists every published immutable bundle with its
-publication date, manifest URL, checksum file, and source commit. The same
-history is available to tooling as JSON:
-
-```text
-https://wordpress.github.io/php-toolkit/wp_native_apis-wasm-extension/index.html
-https://wordpress.github.io/php-toolkit/wp_native_apis-wasm-extension/releases.json
-```
-
-If a bundle needs an additional archive outside GitHub Pages, publish it as a
-GitHub prerelease using a `wp-native-apis-wasm-*` tag and attach the packaged
-manifest, checksum file, and PHP.wasm side module.
-
-The published directory has this shape in the GitHub Pages site:
-
-```text
-wp_native_apis-wasm-extension/
-|-- index.html
-|-- releases.json
-|-- latest/
-| |-- manifest.json
-| |-- SHA256SUMS
-| |-- wp_native_apis-php8.0-jspi.so
-| |-- ...
-| `-- wp_native_apis-php8.5-jspi.so
-`-- /
- |-- manifest.json
- |-- SHA256SUMS
- |-- wp_native_apis-php8.0-jspi.so
- |-- ...
- `-- wp_native_apis-php8.5-jspi.so
-```
-
-When the workflow first sees older SHA-named directories in the release-history
-branch without an existing `releases.json` entry, it imports them into the
-release index and uses the `gh-pages` commit date for their publication date.
-Subsequent releases preserve their recorded publication timestamp.
-
-Use the Blueprint smoke test together with the Playground Query API
-`php-extension` parameter to verify a published PHP.wasm extension bundle.
-`php-extension` must be present in the initial Playground URL because PHP
-extensions load before PHP starts; the Blueprint only writes and runs the smoke
-test.
-
-After publishing the PHP.wasm `manifest.json`, open the main Playground URL
-with a PHP version included in the manifest, the extension manifest, and the
-Blueprint URL:
-
-```text
-https://playground.wordpress.net/?php=8.5&php-extension=&blueprint-url=
-```
-
-For example, a release URL will look like:
-
-```text
-https://playground.wordpress.net/?php=8.5&php-extension=https%3A%2F%2Fwordpress.github.io%2Fphp-toolkit%2Fwp_native_apis-wasm-extension%2Flatest%2Fmanifest.json&blueprint-url=https%3A%2F%2Fraw.githubusercontent.com%2FWordPress%2Fphp-toolkit%2Ftrunk%2Fextensions%2Fnative-apis%2Fplayground%2Fblueprint.json
-```
-
-Expected output:
-
-```text
-wp_native_apis extension version: 0.1.0
-WP_HTML_Native_Tag_Processor: ok
-WP_HTML_Native_Processor: ok
-WordPress\XML\NativeXMLProcessor: ok
-WordPress\DataLiberation\URL\NativeURLInTextProcessor: ok
-PASS: Native API extension classes are available.
-```
-
-The Blueprint lives at `extensions/native-apis/playground/blueprint.json`. It
-writes a small `native-api-smoke.php` file into Playground and navigates to it.
-The smoke page checks that the four native classes are registered, then runs one
-small HTML tag, HTML processor, XML processor, and URL-in-text operation.
-
-If the smoke page reports missing classes, the selected Playground runtime does
-not include the `wp_native_apis` PHP.wasm extension. Check that the URL includes
-`php-extension=`, the bundle matches the selected PHP version, and
-the extension was built for the JSPI PHP.wasm ABI instead of the host PHP ABI.
-Custom PHP.wasm extensions require a JSPI-capable browser runtime; non-JSPI
-runtimes cannot load these side modules.
-
-## Benchmarking
-
-The repository benchmark harness defaults to PHP userland rows so existing
-baseline commands keep their shape:
-
-```bash
-php bin/benchmark-native-apis.php --iterations=50
-```
-
-Native rows normally fail softly with unavailable-class diagnostics so the
-harness remains usable without PHP development headers. Add `--require-native`
-to post-build benchmark commands when missing native classes should produce a
-non-zero exit.
-
-After building and loading the extension, compare PHP and native rows with the
-same workloads:
-
-```bash
-php -d extension=extensions/native-apis/target/release/libwp_native_apis.so \
- bin/benchmark-native-apis.php --iterations=50 --mode=both --require-native
-```
-
-When the extension is loaded but a PHP row should force public wrappers back to
-their PHP fallback classes, pass `--disable-native-defaults`. The harness
-defines `WP_NATIVE_APIS_DISABLE_DEFAULTS` before loading the repository
-bootstrap:
-
-```bash
-php -d extension=extensions/native-apis/target/release/libwp_native_apis.so \
- bin/benchmark-native-apis.php --iterations=50 --mode=php --disable-native-defaults
-```
-
-### Fused and Chunked Workloads
-
-Native-backed public wrappers are fastest when one extension call can cover a
-caller-shaped workflow. The benchmark harness includes rows for these paths:
-
-- HTML tag-prefix sanitizing through document-level removal/update.
-- HTML tag-name scans through compact chunked batches.
-- HTML matching tag-name scans through compact chunked batches.
-- HTML matching tag-name plus attribute extraction through compact chunked batches.
-- HTML matching tag-name plus multi-attribute extraction through compact chunked batches.
-- HTML matching tag-name plus multi-attribute aggregate summaries for link audits.
-- HTML tag inventory summaries for tag, closer, attribute, and unique-name audits.
-- HTML heading inventory summaries for outline and heading-level audits.
-- HTML ID inventory summaries for unique and duplicate ID audits.
-- HTML attribute inventory summaries for attribute-name and decoded-value audits.
-- HTML data-attribute inventory summaries for `data-*` usage audits.
-- HTML ARIA attribute inventory summaries for `aria-*` accessibility audits.
-- HTML class inventory summaries for class-attribute, class-name, and unique-class audits.
-- HTML resource inventory summaries for common `href` and `src` link/media audits.
-- HTML image inventory summaries for image source, alt-text, and dimension audits.
-- HTML form inventory summaries for form/control name audits.
-- HTML tag-prefix count batches for incremental callers that only need aggregate counts.
-- HTML tag-prefix summary scans through compact chunked batches.
-- HTML processor token scans through compact chunked batches.
-- XML token stream summaries and compact token-summary batches.
-- XML streaming factory and reentrancy cursor support for direct native
- processor instances.
-- XML document, attribute, ID, content, and import inventory summaries through
- direct source scans.
-- XML tag scans through compact chunked batches.
-- XML tag count scans through compact chunked count batches.
-- XML namespace/local-name tag scans through compact matching-tag batches.
-- XML namespace/local-name tag counts through compact matching-tag count batches.
-- XML namespace/local-name tag summaries through direct source scans.
-- XML tag, prefix, and sanitizer summaries through direct source scans.
-- URL-in-text scans through a direct native plain-text URL candidate processor,
- with public `URLInTextProcessor` rows preserving WHATWG validation.
-The compact batch APIs return strings with `\x1f` field separators and `\x1e`
-record separators. They are intended for callers that need incremental
-processing but can aggregate without building one PHP array per tag or token.
-Array-returning batch wrappers remain available for clearer application code.
-
-Current benchmark claims should be scoped to these fused and chunked workflow
-rows. Generic public `next_tag()` / `next_token()` loops remain compatibility
-paths and still pay per-item PHP/native crossing overhead; use the aggregate
-benchmark rows to distinguish those loops from target-clearing caller-shaped
-APIs.
-
-## Current Scope
-
-The Rust implementation currently provides parser kernels and native class
-stubs for the first conformance slice:
-
-- HTML tag iteration, tag-name reads, first-slice namespace and qualified-name
- reads, virtual-token status, last-error and unsupported-exception
- diagnostics, syntax-level self-closing flag reads, closer-expectation status,
- attribute reads/removals, decoded class-list and class-membership reads,
- normalization/serialization through the PHP fragment serializer, public native-default token/tag, summary-batch, count-batch, short-batch exhaustion, and aggregate completion-state alignment, text subdivision, modifiable text mutation, DOCTYPE info access, full-parser factory, processor stepping, namespace switching, attribute setting, class additions/removals, prefixed attribute-name scans, public tag-summary,
- tag-prefix compact summary,
- matching-tag summary, matching-tag attribute summary, and matching-tag
- multi-attribute summary batches, complete-input pause status, bookmark
- lifecycle methods, and static
- void/special-category checks, including first-slice character reference
- decoding for numeric and common named references.
-- HTML token iteration via the native processor stub, including text, comment,
- RAWTEXT, and RCDATA token text, qualified-name reads, decoded class reads,
- complete-input pause status, prefixed attribute-name count reads and
- aggregate summaries, tag-prefix summary batches, compact tag-prefix summary
- batch aliases, compact tag-prefix count batches, tag-inventory aggregate
- summaries, current-token and document-level prefixed attribute removal,
- full-parser factory, namespace switching, attribute setting, class additions/removals, PHP serializer-backed normalization/serialization for public compatibility including started-processor rejection after native token advancement, updated HTML serialization and string-cast serialization, breadcrumbs, breadcrumb matching, public
- token-summary with native-default full/final-short batch completion-state
- alignment, structured tag-summary, and compact
- tag-summary/matching-tag summary/matching-tag attribute summary/matching-tag
- multi-attribute summary batches, structured matching-tag summary,
- matching-tag attribute summary, and matching-tag multi-attribute summary
- batches, matching-tag attribute aggregate summaries, and bookmark lifecycle
- methods.
-- XML tag iteration, token names, token types, local-name/namespace reads,
- resolved namespaced attribute reads, text/comment tokens, breadcrumbs,
- bookmark lifecycle methods, complete-input status and append rejection,
- structural/metadata/payload/content/import inventory completion-state alignment,
- token/tag/matching-tag/prefixed-attribute aggregate completion-state alignment,
- public token-summary, tag-summary, matching-tag summary, and count batch
- completion-state alignment, current depth, and malformed document diagnostics.
-- URL-in-text candidate scanning for HTTP, HTTPS, protocol-relative, and bare
- domain references, including trailing punctuation trimming, malformed port
- truncation, replacement serialization, and ASCII-only public wrapper defaults
- that keep the existing WHATWG parser as the fine sieve.
-
-The next milestone should keep extending shared PHPUnit conformance providers
-against the loaded extension and continue targeting caller-shaped fused
-workloads where native code can keep hot scans out of PHP userland.
diff --git a/extensions/native-apis/build-extension.sh b/extensions/native-apis/build-extension.sh
deleted file mode 100755
index d14007a40..000000000
--- a/extensions/native-apis/build-extension.sh
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-cd "${script_dir}"
-
-php_config="${PHP_CONFIG:-php-config}"
-
-if ! command -v cargo >/dev/null 2>&1; then
- echo "Rust cargo was not found in PATH." >&2
- echo "Install Rust, then rerun this script." >&2
- exit 1
-fi
-
-if ! command -v "${php_config}" >/dev/null 2>&1; then
- echo "php-config was not found." >&2
- echo "Install PHP development headers or set PHP_CONFIG=/path/to/php-config." >&2
- echo "Example: PHP_CONFIG=/usr/bin/php-config LIBCLANG_PATH=/path/to/libclang/lib ./build-extension.sh" >&2
- exit 1
-fi
-
-if ! command -v clang >/dev/null 2>&1 && [ -z "${LIBCLANG_PATH:-}" ]; then
- echo "clang was not found and LIBCLANG_PATH is not set." >&2
- echo "Install clang/libclang or set LIBCLANG_PATH to the directory containing libclang." >&2
- exit 1
-fi
-
-PHP_CONFIG="$(command -v "${php_config}")"
-export PHP_CONFIG
-
-cargo build --release --features php-extension
-
-cat <<'MESSAGE'
-Native API extension built.
-
-Load it with:
-php -d extension=extensions/native-apis/target/release/libwp_native_apis.so extensions/native-apis/tests/verify-native-apis.php
-MESSAGE
diff --git a/extensions/native-apis/build-playground-extension.sh b/extensions/native-apis/build-playground-extension.sh
deleted file mode 100755
index e67025fb1..000000000
--- a/extensions/native-apis/build-playground-extension.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-repo_root="$(cd "${script_dir}/../.." && pwd)"
-
-php_versions="${PHP_WASM_VERSIONS:-8.0,8.1,8.2,8.3,8.4,8.5}"
-out_dir="${1:-${repo_root}/build/wp_native_apis-wasm-extension}"
-
-cd "${repo_root}"
-
-npx --yes @php-wasm/compile-extension \
- --prepare-image \
- --php-versions "${php_versions}" \
- --jobs 1
-
-npx --yes @php-wasm/compile-extension \
- --source ./extensions/native-apis \
- --name wp_native_apis \
- --php-versions "${php_versions}" \
- --out "${out_dir}" \
- --jobs 1
-
-cat <
-#include
-
-typedef struct {
- char *source;
- size_t source_len;
- size_t cursor;
- char current_name[64];
- char current_url[256];
- zend_bool current_had_protocol;
- zend_object std;
-} wp_native_smoke_object;
-
-static zend_class_entry *wp_native_html_tag_processor_ce;
-static zend_class_entry *wp_native_html_processor_ce;
-static zend_class_entry *wp_native_xml_processor_ce;
-static zend_class_entry *wp_native_url_processor_ce;
-static zend_object_handlers wp_native_smoke_object_handlers;
-
-
-static zend_bool
-wp_native_ascii_is_space( char c ) {
- return ' ' == c || '\t' == c || '\n' == c || '\r' == c || '\f' == c;
-}
-
-static zend_bool
-wp_native_ascii_is_alnum( char c ) {
- return ( 'a' <= c && 'z' >= c ) || ( 'A' <= c && 'Z' >= c ) || ( '0' <= c && '9' >= c );
-}
-
-static char
-wp_native_ascii_lower( char c ) {
- if ( 'A' <= c && 'Z' >= c ) {
- return c + ( 'a' - 'A' );
- }
- return c;
-}
-
-static char
-wp_native_ascii_upper( char c ) {
- if ( 'a' <= c && 'z' >= c ) {
- return c - ( 'a' - 'A' );
- }
- return c;
-}
-
-static const char *
-wp_native_find_char( const char *start, size_t length, char needle ) {
- size_t i;
-
- for ( i = 0; i < length; i++ ) {
- if ( needle == start[ i ] ) {
- return start + i;
- }
- }
-
- return NULL;
-}
-
-static void
-wp_native_copy_bytes( char *target, const char *source, size_t length ) {
- size_t i;
-
- for ( i = 0; i < length; i++ ) {
- target[ i ] = source[ i ];
- }
-}
-
-static zend_bool
-wp_native_ascii_starts_with( const char *value, size_t value_len, const char *prefix, size_t prefix_len ) {
- size_t i;
-
- if ( value_len < prefix_len ) {
- return 0;
- }
-
- for ( i = 0; i < prefix_len; i++ ) {
- if ( wp_native_ascii_lower( value[ i ] ) != wp_native_ascii_lower( prefix[ i ] ) ) {
- return 0;
- }
- }
-
- return 1;
-}
-
-static zend_bool
-wp_native_is_trailing_url_punctuation( char c ) {
- return '.' == c || ',' == c || ';' == c || ':' == c || '!' == c || '?' == c || ')' == c || '"' == c || ']' == c || '}' == c;
-}
-
-static inline wp_native_smoke_object *
-wp_native_smoke_from_object( zend_object *object ) {
- return (wp_native_smoke_object *) ( (char *) object - XtOffsetOf( wp_native_smoke_object, std ) );
-}
-
-#define Z_WP_NATIVE_SMOKE_P( zval_p ) wp_native_smoke_from_object( Z_OBJ_P( zval_p ) )
-
-static zend_object *
-wp_native_smoke_create_object( zend_class_entry *class_entry ) {
- wp_native_smoke_object *object = zend_object_alloc( sizeof( *object ), class_entry );
- memset( object, 0, XtOffsetOf( wp_native_smoke_object, std ) );
-
- zend_object_std_init( &object->std, class_entry );
- object_properties_init( &object->std, class_entry );
- object->std.handlers = &wp_native_smoke_object_handlers;
-
- return &object->std;
-}
-
-static void
-wp_native_smoke_free_object( zend_object *std ) {
- wp_native_smoke_object *object = wp_native_smoke_from_object( std );
-
- if ( NULL != object->source ) {
- efree( object->source );
- }
-
- zend_object_std_dtor( std );
-}
-
-static void
-wp_native_smoke_set_source( wp_native_smoke_object *object, const char *source, size_t source_len ) {
- if ( NULL != object->source ) {
- efree( object->source );
- }
-
- object->source = estrndup( source, source_len );
- object->source_len = source_len;
- object->cursor = 0;
- object->current_name[0] = '\0';
- object->current_url[0] = '\0';
- object->current_had_protocol = 0;
-}
-
-static zend_bool
-wp_native_ascii_ieq( const char *left, size_t left_len, const char *right, size_t right_len ) {
- size_t i;
-
- if ( left_len != right_len ) {
- return 0;
- }
-
- for ( i = 0; i < left_len; i++ ) {
- if ( wp_native_ascii_lower( left[ i ] ) != wp_native_ascii_lower( right[ i ] ) ) {
- return 0;
- }
- }
-
- return 1;
-}
-
-static void
-wp_native_copy_upper_name( char *target, size_t target_len, const char *name, size_t name_len ) {
- size_t i;
- size_t limit = name_len;
-
- if ( 0 == target_len ) {
- return;
- }
-
- if ( limit >= target_len ) {
- limit = target_len - 1;
- }
-
- for ( i = 0; i < limit; i++ ) {
- target[ i ] = wp_native_ascii_upper( name[ i ] );
- }
- target[ limit ] = '\0';
-}
-
-static zend_bool
-wp_native_tag_has_class( const char *tag_start, const char *tag_end, const char *class_name, size_t class_name_len ) {
- const char *cursor = tag_start;
-
- while ( cursor + 5 < tag_end ) {
- if (
- ( 'c' == cursor[0] || 'C' == cursor[0] ) &&
- ( 'l' == cursor[1] || 'L' == cursor[1] ) &&
- ( 'a' == cursor[2] || 'A' == cursor[2] ) &&
- ( 's' == cursor[3] || 'S' == cursor[3] ) &&
- ( 's' == cursor[4] || 'S' == cursor[4] )
- ) {
- const char *value;
- const char *value_end;
-
- cursor += 5;
- while ( cursor < tag_end && wp_native_ascii_is_space( *cursor ) ) {
- cursor++;
- }
- if ( cursor >= tag_end || '=' != *cursor ) {
- continue;
- }
- cursor++;
- while ( cursor < tag_end && wp_native_ascii_is_space( *cursor ) ) {
- cursor++;
- }
- if ( cursor >= tag_end || ( '"' != *cursor && '\'' != *cursor ) ) {
- continue;
- }
-
- value = ++cursor;
- value_end = wp_native_find_char( value, tag_end - value, cursor[-1] );
- if ( NULL == value_end ) {
- value_end = tag_end;
- }
-
- while ( value < value_end ) {
- const char *part = value;
- while ( part < value_end && wp_native_ascii_is_space( *part ) ) {
- part++;
- }
- value = part;
- while ( value < value_end && ! wp_native_ascii_is_space( *value ) ) {
- value++;
- }
- if ( wp_native_ascii_ieq( part, value - part, class_name, class_name_len ) ) {
- return 1;
- }
- }
- }
- cursor++;
- }
-
- return 0;
-}
-
-static zend_bool
-wp_native_html_next_tag( wp_native_smoke_object *object, zval *query ) {
- const char *requested_tag = NULL;
- size_t requested_tag_len = 0;
- const char *requested_class = NULL;
- size_t requested_class_len = 0;
-
- if ( NULL != query && IS_ARRAY == Z_TYPE_P( query ) ) {
- zval *tag_name = zend_hash_str_find( Z_ARRVAL_P( query ), "tag_name", sizeof( "tag_name" ) - 1 );
- zval *class_name = zend_hash_str_find( Z_ARRVAL_P( query ), "class_name", sizeof( "class_name" ) - 1 );
-
- if ( NULL != tag_name && IS_STRING == Z_TYPE_P( tag_name ) ) {
- requested_tag = Z_STRVAL_P( tag_name );
- requested_tag_len = Z_STRLEN_P( tag_name );
- }
- if ( NULL != class_name && IS_STRING == Z_TYPE_P( class_name ) ) {
- requested_class = Z_STRVAL_P( class_name );
- requested_class_len = Z_STRLEN_P( class_name );
- }
- }
-
- while ( object->cursor < object->source_len ) {
- const char *tag_start;
- const char *tag_end;
- const char *name;
- size_t name_len;
-
- tag_start = wp_native_find_char( object->source + object->cursor, object->source_len - object->cursor, '<' );
- if ( NULL == tag_start ) {
- object->cursor = object->source_len;
- return 0;
- }
-
- object->cursor = ( tag_start - object->source ) + 1;
- if ( object->cursor >= object->source_len || '/' == object->source[ object->cursor ] || '!' == object->source[ object->cursor ] || '?' == object->source[ object->cursor ] ) {
- continue;
- }
-
- name = object->source + object->cursor;
- while ( object->cursor < object->source_len ) {
- char c = object->source[ object->cursor ];
- if ( ! ( wp_native_ascii_is_alnum( c ) || ':' == c || '-' == c ) ) {
- break;
- }
- object->cursor++;
- }
-
- name_len = object->source + object->cursor - name;
- tag_end = wp_native_find_char( object->source + object->cursor, object->source_len - object->cursor, '>' );
- if ( NULL == tag_end ) {
- tag_end = object->source + object->source_len;
- }
- object->cursor = ( tag_end - object->source ) + ( tag_end < object->source + object->source_len ? 1 : 0 );
-
- if ( 0 == name_len ) {
- continue;
- }
- if ( NULL != requested_tag && ! wp_native_ascii_ieq( name, name_len, requested_tag, requested_tag_len ) ) {
- continue;
- }
- if ( NULL != requested_class && ! wp_native_tag_has_class( name + name_len, tag_end, requested_class, requested_class_len ) ) {
- continue;
- }
-
- wp_native_copy_upper_name( object->current_name, sizeof( object->current_name ), name, name_len );
- return 1;
- }
-
- return 0;
-}
-
-PHP_METHOD( WP_HTML_Native_Tag_Processor, __construct ) {
- char *html;
- size_t html_len;
-
- ZEND_PARSE_PARAMETERS_START( 1, 1 )
- Z_PARAM_STRING( html, html_len )
- ZEND_PARSE_PARAMETERS_END();
-
- wp_native_smoke_set_source( Z_WP_NATIVE_SMOKE_P( getThis() ), html, html_len );
-}
-
-PHP_METHOD( WP_HTML_Native_Tag_Processor, next_tag ) {
- zval *query = NULL;
-
- ZEND_PARSE_PARAMETERS_START( 0, 1 )
- Z_PARAM_OPTIONAL
- Z_PARAM_ZVAL( query )
- ZEND_PARSE_PARAMETERS_END();
-
- RETURN_BOOL( wp_native_html_next_tag( Z_WP_NATIVE_SMOKE_P( getThis() ), query ) );
-}
-
-PHP_METHOD( WP_HTML_Native_Tag_Processor, get_tag ) {
- wp_native_smoke_object *object = Z_WP_NATIVE_SMOKE_P( getThis() );
-
- ZEND_PARSE_PARAMETERS_NONE();
-
- if ( '\0' == object->current_name[0] ) {
- RETURN_NULL();
- }
- RETURN_STRING( object->current_name );
-}
-
-PHP_METHOD( WP_HTML_Native_Processor, create_fragment ) {
- char *html;
- size_t html_len;
- wp_native_smoke_object *object;
-
- ZEND_PARSE_PARAMETERS_START( 1, 1 )
- Z_PARAM_STRING( html, html_len )
- ZEND_PARSE_PARAMETERS_END();
-
- object_init_ex( return_value, wp_native_html_processor_ce );
- object = Z_WP_NATIVE_SMOKE_P( return_value );
- wp_native_smoke_set_source( object, html, html_len );
-}
-
-PHP_METHOD( WP_HTML_Native_Processor, next_tag ) {
- zval *query = NULL;
-
- ZEND_PARSE_PARAMETERS_START( 0, 1 )
- Z_PARAM_OPTIONAL
- Z_PARAM_ZVAL( query )
- ZEND_PARSE_PARAMETERS_END();
-
- RETURN_BOOL( wp_native_html_next_tag( Z_WP_NATIVE_SMOKE_P( getThis() ), query ) );
-}
-
-PHP_METHOD( WP_HTML_Native_Processor, get_tag ) {
- wp_native_smoke_object *object = Z_WP_NATIVE_SMOKE_P( getThis() );
-
- ZEND_PARSE_PARAMETERS_NONE();
-
- if ( '\0' == object->current_name[0] ) {
- RETURN_NULL();
- }
- RETURN_STRING( object->current_name );
-}
-
-static zend_bool
-wp_native_xml_next_tag( wp_native_smoke_object *object, const char *requested, size_t requested_len ) {
- while ( object->cursor < object->source_len ) {
- const char *tag_start;
- const char *name;
- const char *local;
- size_t name_len;
- size_t local_len;
-
- tag_start = wp_native_find_char( object->source + object->cursor, object->source_len - object->cursor, '<' );
- if ( NULL == tag_start ) {
- object->cursor = object->source_len;
- return 0;
- }
-
- object->cursor = ( tag_start - object->source ) + 1;
- if ( object->cursor >= object->source_len || '/' == object->source[ object->cursor ] || '!' == object->source[ object->cursor ] || '?' == object->source[ object->cursor ] ) {
- continue;
- }
-
- name = object->source + object->cursor;
- while ( object->cursor < object->source_len ) {
- char c = object->source[ object->cursor ];
- if ( ! ( wp_native_ascii_is_alnum( c ) || ':' == c || '-' == c || '_' == c ) ) {
- break;
- }
- object->cursor++;
- }
-
- name_len = object->source + object->cursor - name;
- local = wp_native_find_char( name, name_len, ':' );
- if ( NULL == local ) {
- local = name;
- local_len = name_len;
- } else {
- local++;
- local_len = name + name_len - local;
- }
-
- if ( NULL != requested && ! wp_native_ascii_ieq( local, local_len, requested, requested_len ) ) {
- continue;
- }
-
- if ( local_len >= sizeof( object->current_name ) ) {
- local_len = sizeof( object->current_name ) - 1;
- }
- wp_native_copy_bytes( object->current_name, local, local_len );
- object->current_name[ local_len ] = '\0';
- return 1;
- }
-
- return 0;
-}
-
-PHP_METHOD( NativeXMLProcessor, create_from_string ) {
- char *xml;
- size_t xml_len;
- wp_native_smoke_object *object;
-
- ZEND_PARSE_PARAMETERS_START( 1, 1 )
- Z_PARAM_STRING( xml, xml_len )
- ZEND_PARSE_PARAMETERS_END();
-
- object_init_ex( return_value, wp_native_xml_processor_ce );
- object = Z_WP_NATIVE_SMOKE_P( return_value );
- wp_native_smoke_set_source( object, xml, xml_len );
-}
-
-PHP_METHOD( NativeXMLProcessor, next_tag ) {
- char *tag_name = NULL;
- size_t tag_name_len = 0;
-
- ZEND_PARSE_PARAMETERS_START( 0, 1 )
- Z_PARAM_OPTIONAL
- Z_PARAM_STRING( tag_name, tag_name_len )
- ZEND_PARSE_PARAMETERS_END();
-
- RETURN_BOOL( wp_native_xml_next_tag( Z_WP_NATIVE_SMOKE_P( getThis() ), tag_name, tag_name_len ) );
-}
-
-PHP_METHOD( NativeXMLProcessor, get_tag_local_name ) {
- wp_native_smoke_object *object = Z_WP_NATIVE_SMOKE_P( getThis() );
-
- ZEND_PARSE_PARAMETERS_NONE();
-
- if ( '\0' == object->current_name[0] ) {
- RETURN_NULL();
- }
- RETURN_STRING( object->current_name );
-}
-
-static zend_bool
-wp_native_url_next( wp_native_smoke_object *object ) {
- while ( object->cursor < object->source_len ) {
- size_t start = object->cursor;
- size_t end;
- zend_bool had_protocol = 0;
-
- while ( start < object->source_len && wp_native_ascii_is_space( object->source[ start ] ) ) {
- start++;
- }
-
- end = start;
- while ( end < object->source_len && ! wp_native_ascii_is_space( object->source[ end ] ) ) {
- end++;
- }
- object->cursor = end + ( end < object->source_len ? 1 : 0 );
-
- if ( end <= start ) {
- continue;
- }
-
- while ( end > start && wp_native_is_trailing_url_punctuation( object->source[ end - 1 ] ) ) {
- end--;
- }
-
- if ( wp_native_ascii_starts_with( object->source + start, end - start, "http://", 7 ) ) {
- had_protocol = 1;
- } else if ( wp_native_ascii_starts_with( object->source + start, end - start, "https://", 8 ) ) {
- had_protocol = 1;
- } else if ( NULL == wp_native_find_char( object->source + start, end - start, '.' ) ) {
- continue;
- }
-
- if ( end - start >= sizeof( object->current_url ) ) {
- end = start + sizeof( object->current_url ) - 1;
- }
- wp_native_copy_bytes( object->current_url, object->source + start, end - start );
- object->current_url[ end - start ] = '\0';
- object->current_had_protocol = had_protocol;
- return 1;
- }
-
- return 0;
-}
-
-PHP_METHOD( NativeURLInTextProcessor, __construct ) {
- char *text;
- size_t text_len;
- char *base_url = NULL;
- size_t base_url_len = 0;
-
- ZEND_PARSE_PARAMETERS_START( 1, 2 )
- Z_PARAM_STRING( text, text_len )
- Z_PARAM_OPTIONAL
- Z_PARAM_STRING( base_url, base_url_len )
- ZEND_PARSE_PARAMETERS_END();
-
- (void) base_url;
- (void) base_url_len;
-
- wp_native_smoke_set_source( Z_WP_NATIVE_SMOKE_P( getThis() ), text, text_len );
-}
-
-PHP_METHOD( NativeURLInTextProcessor, next_url ) {
- ZEND_PARSE_PARAMETERS_NONE();
- RETURN_BOOL( wp_native_url_next( Z_WP_NATIVE_SMOKE_P( getThis() ) ) );
-}
-
-PHP_METHOD( NativeURLInTextProcessor, get_raw_url ) {
- wp_native_smoke_object *object = Z_WP_NATIVE_SMOKE_P( getThis() );
-
- ZEND_PARSE_PARAMETERS_NONE();
-
- if ( '\0' == object->current_url[0] ) {
- RETURN_NULL();
- }
- RETURN_STRING( object->current_url );
-}
-
-PHP_METHOD( NativeURLInTextProcessor, had_protocol ) {
- ZEND_PARSE_PARAMETERS_NONE();
- RETURN_BOOL( Z_WP_NATIVE_SMOKE_P( getThis() )->current_had_protocol );
-}
-
-PHP_FUNCTION( wp_native_apis_extension_version ) {
- ZEND_PARSE_PARAMETERS_NONE();
- RETURN_STRING( PHP_WP_NATIVE_APIS_VERSION );
-}
-
-ZEND_BEGIN_ARG_INFO_EX( arginfo_wp_native_string_ctor, 0, 0, 1 )
- ZEND_ARG_TYPE_INFO( 0, input, IS_STRING, 0 )
-ZEND_END_ARG_INFO()
-
-ZEND_BEGIN_ARG_INFO_EX( arginfo_wp_native_url_ctor, 0, 0, 1 )
- ZEND_ARG_TYPE_INFO( 0, text, IS_STRING, 0 )
- ZEND_ARG_TYPE_INFO( 0, base_url, IS_STRING, 1 )
-ZEND_END_ARG_INFO()
-
-ZEND_BEGIN_ARG_INFO_EX( arginfo_wp_native_next_tag, 0, 0, 0 )
- ZEND_ARG_TYPE_INFO( 0, query, IS_ARRAY, 1 )
-ZEND_END_ARG_INFO()
-
-ZEND_BEGIN_ARG_INFO_EX( arginfo_wp_native_next_xml_tag, 0, 0, 0 )
- ZEND_ARG_TYPE_INFO( 0, tag_name, IS_STRING, 1 )
-ZEND_END_ARG_INFO()
-
-ZEND_BEGIN_ARG_INFO_EX( arginfo_wp_native_void, 0, 0, 0 )
-ZEND_END_ARG_INFO()
-
-ZEND_BEGIN_ARG_INFO_EX( arginfo_wp_native_create_fragment, 0, 0, 1 )
- ZEND_ARG_TYPE_INFO( 0, html, IS_STRING, 0 )
-ZEND_END_ARG_INFO()
-
-ZEND_BEGIN_ARG_INFO_EX( arginfo_wp_native_create_from_string, 0, 0, 1 )
- ZEND_ARG_TYPE_INFO( 0, xml, IS_STRING, 0 )
-ZEND_END_ARG_INFO()
-
-static const zend_function_entry wp_native_html_tag_processor_methods[] = {
- PHP_ME( WP_HTML_Native_Tag_Processor, __construct, arginfo_wp_native_string_ctor, ZEND_ACC_PUBLIC )
- PHP_ME( WP_HTML_Native_Tag_Processor, next_tag, arginfo_wp_native_next_tag, ZEND_ACC_PUBLIC )
- PHP_ME( WP_HTML_Native_Tag_Processor, get_tag, arginfo_wp_native_void, ZEND_ACC_PUBLIC )
- PHP_FE_END
-};
-
-static const zend_function_entry wp_native_html_processor_methods[] = {
- PHP_ME( WP_HTML_Native_Processor, create_fragment, arginfo_wp_native_create_fragment, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC )
- PHP_ME( WP_HTML_Native_Processor, next_tag, arginfo_wp_native_next_tag, ZEND_ACC_PUBLIC )
- PHP_ME( WP_HTML_Native_Processor, get_tag, arginfo_wp_native_void, ZEND_ACC_PUBLIC )
- PHP_FE_END
-};
-
-static const zend_function_entry wp_native_xml_processor_methods[] = {
- PHP_ME( NativeXMLProcessor, create_from_string, arginfo_wp_native_create_from_string, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC )
- PHP_ME( NativeXMLProcessor, next_tag, arginfo_wp_native_next_xml_tag, ZEND_ACC_PUBLIC )
- PHP_ME( NativeXMLProcessor, get_tag_local_name, arginfo_wp_native_void, ZEND_ACC_PUBLIC )
- PHP_FE_END
-};
-
-static const zend_function_entry wp_native_url_processor_methods[] = {
- PHP_ME( NativeURLInTextProcessor, __construct, arginfo_wp_native_url_ctor, ZEND_ACC_PUBLIC )
- PHP_ME( NativeURLInTextProcessor, next_url, arginfo_wp_native_void, ZEND_ACC_PUBLIC )
- PHP_ME( NativeURLInTextProcessor, get_raw_url, arginfo_wp_native_void, ZEND_ACC_PUBLIC )
- PHP_ME( NativeURLInTextProcessor, had_protocol, arginfo_wp_native_void, ZEND_ACC_PUBLIC )
- PHP_FE_END
-};
-
-static const zend_function_entry wp_native_apis_functions[] = {
- PHP_FE( wp_native_apis_extension_version, arginfo_wp_native_void )
- PHP_FE_END
-};
-
-PHP_MINIT_FUNCTION( wp_native_apis ) {
- zend_class_entry class_entry;
-
- memcpy( &wp_native_smoke_object_handlers, &std_object_handlers, sizeof( zend_object_handlers ) );
- wp_native_smoke_object_handlers.offset = XtOffsetOf( wp_native_smoke_object, std );
- wp_native_smoke_object_handlers.free_obj = wp_native_smoke_free_object;
- wp_native_smoke_object_handlers.clone_obj = NULL;
-
- INIT_CLASS_ENTRY( class_entry, "WP_HTML_Native_Tag_Processor", wp_native_html_tag_processor_methods );
- wp_native_html_tag_processor_ce = zend_register_internal_class_ex( &class_entry, NULL );
- wp_native_html_tag_processor_ce->create_object = wp_native_smoke_create_object;
-
- INIT_CLASS_ENTRY( class_entry, "WP_HTML_Native_Processor", wp_native_html_processor_methods );
- wp_native_html_processor_ce = zend_register_internal_class_ex( &class_entry, NULL );
- wp_native_html_processor_ce->create_object = wp_native_smoke_create_object;
-
- INIT_NS_CLASS_ENTRY( class_entry, "WordPress\\XML", "NativeXMLProcessor", wp_native_xml_processor_methods );
- wp_native_xml_processor_ce = zend_register_internal_class_ex( &class_entry, NULL );
- wp_native_xml_processor_ce->create_object = wp_native_smoke_create_object;
-
- INIT_NS_CLASS_ENTRY( class_entry, "WordPress\\DataLiberation\\URL", "NativeURLInTextProcessor", wp_native_url_processor_methods );
- wp_native_url_processor_ce = zend_register_internal_class_ex( &class_entry, NULL );
- wp_native_url_processor_ce->create_object = wp_native_smoke_create_object;
-
- return SUCCESS;
-}
-
-PHP_MINFO_FUNCTION( wp_native_apis ) {
- php_info_print_table_start();
- php_info_print_table_row( 2, "wp_native_apis", "enabled" );
- php_info_print_table_row( 2, "playground", "enabled" );
- php_info_print_table_end();
-}
-
-zend_module_entry wp_native_apis_module_entry = {
- STANDARD_MODULE_HEADER,
- "wp_native_apis",
- wp_native_apis_functions,
- PHP_MINIT( wp_native_apis ),
- NULL,
- NULL,
- NULL,
- PHP_MINFO( wp_native_apis ),
- PHP_WP_NATIVE_APIS_VERSION,
- STANDARD_MODULE_PROPERTIES
-};
-
-#ifdef COMPILE_DL_WP_NATIVE_APIS
-# ifdef ZTS
-ZEND_TSRMLS_CACHE_DEFINE()
-# endif
-ZEND_GET_MODULE( wp_native_apis )
-#endif
diff --git a/extensions/native-apis/playground/blueprint.json b/extensions/native-apis/playground/blueprint.json
deleted file mode 100644
index c684a32f2..000000000
--- a/extensions/native-apis/playground/blueprint.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "$schema": "https://playground.wordpress.net/blueprint-schema.json",
- "landingPage": "/native-api-smoke.php",
- "preferredVersions": {
- "php": "8.4",
- "wp": "latest"
- },
- "meta": {
- "title": "WordPress Native APIs Playground test",
- "author": "WordPress",
- "description": "Verifies that WordPress Playground loaded the wp_native_apis PHP.wasm extension and shows quick browser-side timing numbers."
- },
- "steps": [
- {
- "step": "writeFile",
- "path": "/wordpress/native-api-smoke.php",
- "data": " microtime( true ) - $start,\n\t\t'result' => $result,\n\t);\n}\n\nfunction native_api_load_wordpress_html_api() {\n\tif ( class_exists( 'WP_HTML_Tag_Processor', false ) || ! defined( 'ABSPATH' ) || ! defined( 'WPINC' ) ) {\n\t\treturn;\n\t}\n\n\t$files = array(\n\t\tABSPATH . WPINC . '/html-api/class-wp-html-attribute-token.php',\n\t\tABSPATH . WPINC . '/html-api/class-wp-html-span.php',\n\t\tABSPATH . WPINC . '/html-api/class-wp-html-text-replacement.php',\n\t\tABSPATH . WPINC . '/html-api/class-wp-token-map.php',\n\t\tABSPATH . WPINC . '/html-api/class-wp-html-tag-processor.php',\n\t);\n\n\tforeach ( $files as $file ) {\n\t\tif ( file_exists( $file ) ) {\n\t\t\trequire_once $file;\n\t\t}\n\t}\n}\n\nfunction native_api_repeat_html( $count ) {\n\t$html = '';\n\tfor ( $i = 0; $i < $count; $i++ ) {\n\t\t$html .= 'Post ' . $i . ' Text
Read more ';\n\t}\n\treturn $html;\n}\n\nfunction native_api_repeat_xml( $count ) {\n\t$xml = '';\n\tfor ( $i = 0; $i < $count; $i++ ) {\n\t\t$xml .= 'Item ' . $i . ' ';\n\t}\n\treturn $xml . ' ';\n}\n\nfunction native_api_repeat_text( $count ) {\n\t$text = '';\n\tfor ( $i = 0; $i < $count; $i++ ) {\n\t\t$text .= 'Read example.com/post-' . $i . ' and https://wordpress.org/news/' . $i . ' today. ';\n\t}\n\treturn $text;\n}\n\nfunction native_api_benchmark_html( $html, $rounds ) {\n\t$native = native_api_time_callable(\n\t\tfunction () use ( $html, $rounds ) {\n\t\t\t$total = 0;\n\t\t\tfor ( $round = 0; $round < $rounds; $round++ ) {\n\t\t\t\t$processor = new WP_HTML_Native_Tag_Processor( $html );\n\t\t\t\twhile ( $processor->next_tag( array( 'tag_name' => 'a' ) ) ) {\n\t\t\t\t\t$total++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn $total;\n\t\t}\n\t);\n\n\t$php = null;\n\tnative_api_load_wordpress_html_api();\n\tif ( class_exists( 'WP_HTML_Tag_Processor' ) ) {\n\t\t$php = native_api_time_callable(\n\t\t\tfunction () use ( $html, $rounds ) {\n\t\t\t\t$total = 0;\n\t\t\t\tfor ( $round = 0; $round < $rounds; $round++ ) {\n\t\t\t\t\t$processor = new WP_HTML_Tag_Processor( $html );\n\t\t\t\t\twhile ( $processor->next_tag( array( 'tag_name' => 'a' ) ) ) {\n\t\t\t\t\t\t$total++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn $total;\n\t\t\t}\n\t\t);\n\t}\n\n\treturn array(\n\t\t'label' => 'HTML link scan',\n\t\t'work' => number_format( $native['result'] ) . ' links scanned',\n\t\t'native' => $native['seconds'],\n\t\t'php' => $php ? $php['seconds'] : null,\n\t\t'speedup' => $php && $native['seconds'] > 0 ? $php['seconds'] / $native['seconds'] : null,\n\t);\n}\n\nfunction native_api_benchmark_xml( $xml, $rounds ) {\n\t$native = native_api_time_callable(\n\t\tfunction () use ( $xml, $rounds ) {\n\t\t\t$total = 0;\n\t\t\t$xml_class = 'WordPress\\\\XML\\\\NativeXMLProcessor';\n\t\t\tfor ( $round = 0; $round < $rounds; $round++ ) {\n\t\t\t\t$processor = $xml_class::create_from_string( $xml );\n\t\t\t\twhile ( $processor->next_tag( 'item' ) ) {\n\t\t\t\t\t$total++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn $total;\n\t\t}\n\t);\n\n\treturn array(\n\t\t'label' => 'XML item scan',\n\t\t'work' => number_format( $native['result'] ) . ' items scanned',\n\t\t'native' => $native['seconds'],\n\t\t'php' => null,\n\t);\n}\n\nfunction native_api_benchmark_url( $text, $rounds ) {\n\t$native = native_api_time_callable(\n\t\tfunction () use ( $text, $rounds ) {\n\t\t\t$total = 0;\n\t\t\t$url_class = 'WordPress\\\\DataLiberation\\\\URL\\\\NativeURLInTextProcessor';\n\t\t\tfor ( $round = 0; $round < $rounds; $round++ ) {\n\t\t\t\t$processor = new $url_class( $text, 'https://wordpress.org' );\n\t\t\t\twhile ( $processor->next_url() ) {\n\t\t\t\t\t$total++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn $total;\n\t\t}\n\t);\n\n\treturn array(\n\t\t'label' => 'URL-in-text scan',\n\t\t'work' => number_format( $native['result'] ) . ' URLs scanned',\n\t\t'native' => $native['seconds'],\n\t\t'php' => null,\n\t);\n}\n\n$classes = array(\n\t'WP_HTML_Native_Tag_Processor',\n\t'WP_HTML_Native_Processor',\n\t'WordPress\\\\XML\\\\NativeXMLProcessor',\n\t'WordPress\\\\DataLiberation\\\\URL\\\\NativeURLInTextProcessor',\n);\n\n$missing = array();\nforeach ( $classes as $class_name ) {\n\tif ( ! class_exists( $class_name, false ) ) {\n\t\t$missing[] = $class_name;\n\t}\n}\n\n$version = function_exists( 'wp_native_apis_extension_version' ) ? wp_native_apis_extension_version() : 'loaded';\n$benchmarks = array();\n$error = '';\n\nif ( empty( $missing ) ) {\n\ttry {\n\t\t$tag_processor = new WP_HTML_Native_Tag_Processor( 'Text
' );\n\t\tif ( ! $tag_processor->next_tag( array( 'tag_name' => 'p', 'class_name' => 'target' ) ) || 'P' !== $tag_processor->get_tag() ) {\n\t\t\tthrow new Exception( 'WP_HTML_Native_Tag_Processor did not find the target paragraph.' );\n\t\t}\n\n\t\t$html_processor = WP_HTML_Native_Processor::create_fragment( '' );\n\t\tif ( ! is_object( $html_processor ) || ! $html_processor->next_tag( array( 'tag_name' => 'a' ) ) || 'A' !== $html_processor->get_tag() ) {\n\t\t\tthrow new Exception( 'WP_HTML_Native_Processor did not find the link in a fragment.' );\n\t\t}\n\n\t\t$xml_class = 'WordPress\\\\XML\\\\NativeXMLProcessor';\n\t\t$xml_processor = $xml_class::create_from_string( ' ' );\n\t\tif ( ! is_object( $xml_processor ) || ! $xml_processor->next_tag( 'item' ) || 'item' !== $xml_processor->get_tag_local_name() ) {\n\t\t\tthrow new Exception( 'NativeXMLProcessor did not find the item element.' );\n\t\t}\n\n\t\t$url_class = 'WordPress\\\\DataLiberation\\\\URL\\\\NativeURLInTextProcessor';\n\t\t$url_processor = new $url_class( 'Visit example.com/docs now.', 'https://wordpress.org' );\n\t\tif ( ! $url_processor->next_url() || 'example.com/docs' !== $url_processor->get_raw_url() || $url_processor->had_protocol() ) {\n\t\t\tthrow new Exception( 'NativeURLInTextProcessor did not find the bare-domain URL.' );\n\t\t}\n\n\t\t$benchmarks[] = native_api_benchmark_html( native_api_repeat_html( 250 ), 8 );\n\t\t$benchmarks[] = native_api_benchmark_xml( native_api_repeat_xml( 350 ), 8 );\n\t\t$benchmarks[] = native_api_benchmark_url( native_api_repeat_text( 250 ), 8 );\n\t} catch ( Exception $exception ) {\n\t\t$error = $exception->getMessage();\n\t\thttp_response_code( 500 );\n\t}\n} else {\n\thttp_response_code( 500 );\n}\n?>\n\n\n\n \n \nNative APIs Playground test \n\n\n\n\n\n\t\u2713 wp_native_apis WASM extension loaded
\n\tNative APIs are running in Playground. \n\tThe php-extension manifest loaded before PHP started, WordPress can see the extension classes, and the smoke tests passed. These quick timings ran inside this browser\u2019s PHP.wasm runtime.
\n\t\n\t\t\n\t\t\t
\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t with native APIs.
\n\t\t\t\t\n\t\t\t\t\tPure PHP: \u00b7 \u00d7 faster here
\n\t\t\t\t\n\t\t\t \n\t\t\n\t
\n\tBrowser timings vary by device and tab load. For repeatable host-extension comparisons, see the CI benchmark snapshot on the release page .
\n\tExtension version:
\n\n\t\u2715 wp_native_apis WASM extension was not loaded
\n\tThe Native APIs extension is missing. \n\t\n\t\t\n\t\t\t
Missing classes:
\n\t\t\n\t\t\n\t\t\t
\n\t\t\n\t\t
This page can only test an extension that was loaded with the Playground php-extension query parameter before PHP started.
\n\t
\n\n \n\n\n"
- }
- ]
-}
diff --git a/extensions/native-apis/src/html.rs b/extensions/native-apis/src/html.rs
deleted file mode 100644
index 63f79926e..000000000
--- a/extensions/native-apis/src/html.rs
+++ /dev/null
@@ -1,8569 +0,0 @@
-#![cfg_attr(not(feature = "php-extension"), allow(dead_code))]
-
-use std::borrow::Cow;
-use std::collections::HashMap;
-
-#[cfg(feature = "php-extension")]
-use ext_php_rs::prelude::*;
-#[cfg(feature = "php-extension")]
-use ext_php_rs::{
- boxed::ZBox,
- types::{ZendHashTable, Zval},
- zend::Function,
-};
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct HtmlTag {
- pub name: String,
- pub token_type: String,
- pub closing: bool,
- pub attributes: HashMap,
- pub attribute_order: Vec,
- pub attribute_name_initials: u32,
- pub source_start: usize,
- pub source_end: usize,
- pub text: String,
- pub comment_type: Option,
- pub full_comment_text: Option,
- pub breadcrumbs: Vec,
- pub depth: usize,
-}
-
-#[cfg(feature = "php-extension")]
-#[php_class]
-#[php(name = "WP_HTML_Native_Tag_Processor")]
-pub struct WpHtmlNativeTagProcessor {
- html: String,
- offset: usize,
- breadcrumbs: Vec,
- parsing_namespace: String,
- synthesize_implied_closers: bool,
- ignore_html_body_starts: bool,
- current: Option,
- removals: Vec,
- removed_attributes: Vec,
- updated_attributes: Vec,
- bookmarks: HashMap,
-}
-
-#[derive(Clone)]
-struct HtmlBookmark {
- offset: usize,
- breadcrumbs: Vec,
- parsing_namespace: String,
- synthesize_implied_closers: bool,
- ignore_html_body_starts: bool,
- current: Option,
-}
-
-struct HtmlTextRemoval {
- start: usize,
- length: usize,
- replacement: String,
-}
-
-struct HtmlRemovedAttribute {
- tag_start: usize,
- tag_end: usize,
- name: String,
-}
-
-struct HtmlUpdatedAttribute {
- tag_start: usize,
- tag_end: usize,
- name: String,
- value: Option,
-}
-
-type HtmlAttributePrefixRemovals = (i64, Vec<(usize, usize)>, Vec);
-
-enum HtmlAttributeValue {
- Boolean,
- String(String),
-}
-
-#[derive(Default)]
-struct HtmlNextTagQuery {
- tag_name: Option,
- class_name: Option,
- match_offset: i64,
- visit_closers: bool,
- breadcrumbs: Option>,
-}
-
-#[cfg(feature = "php-extension")]
-impl WpHtmlNativeTagProcessor {
- fn get_attribute_value(&self, name: &str) -> Option {
- self.current_tag().and_then(|tag| {
- if tag.token_type == "#tag" {
- if self.removed_attributes.iter().any(|removed| {
- removed.tag_start == tag.source_start
- && removed.tag_end == tag.source_end
- && removed.name == name
- }) {
- return None;
- }
-
- if let Some(updated) = self.updated_attributes.iter().rev().find(|updated| {
- updated.tag_start == tag.source_start
- && updated.tag_end == tag.source_end
- && updated.name == name
- }) {
- return match updated.value.as_ref() {
- Some(value) => Some(HtmlAttributeValue::String(value.clone())),
- None => Some(HtmlAttributeValue::Boolean),
- };
- }
-
- tag.attributes
- .get(name)
- .cloned()
- .map(|value| {
- if value.is_empty()
- && !find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- name,
- )
- .unwrap_or(true)
- {
- HtmlAttributeValue::Boolean
- } else {
- HtmlAttributeValue::String(value)
- }
- })
- .or_else(|| {
- match find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- name,
- ) {
- Some(false) => Some(HtmlAttributeValue::Boolean),
- Some(true) => find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- name,
- )
- .map(HtmlAttributeValue::String),
- None => None,
- }
- })
- } else {
- None
- }
- })
- }
-}
-
-#[cfg(feature = "php-extension")]
-#[php_impl]
-#[php(change_method_case = "snake_case")]
-impl WpHtmlNativeTagProcessor {
- pub fn __construct(html: String) -> Self {
- Self {
- html,
- offset: 0,
- breadcrumbs: initial_html_breadcrumbs(),
- parsing_namespace: "html".to_string(),
- synthesize_implied_closers: false,
- ignore_html_body_starts: false,
- current: None,
- removals: Vec::new(),
- removed_attributes: Vec::new(),
- updated_attributes: Vec::new(),
- bookmarks: HashMap::new(),
- }
- }
-
- pub fn supports_public_api() -> bool {
- true
- }
-
- #[php(optional = query)]
- pub fn next_tag(&mut self, query: Option<&Zval>) -> bool {
- let query = html_parse_tag_processor_next_tag_query(query);
- let mut match_offset = query.match_offset.max(1);
-
- while self.advance_to_next_tag_token() {
- if !self.current_html_tag_matches_query(&query, false) {
- continue;
- }
-
- match_offset -= 1;
- if match_offset == 0 {
- return true;
- }
- }
-
- false
- }
-
- pub fn next_tag_any(&mut self, visit_closers: bool, mut match_offset: i64) -> bool {
- if match_offset < 1 {
- match_offset = 1;
- }
-
- while self.advance_to_next_tag_token() {
- if let Some(tag) = self.current_tag() {
- if visit_closers || !tag.closing {
- match_offset -= 1;
- if match_offset == 0 {
- return true;
- }
- }
- }
- }
-
- false
- }
-
- pub fn next_tag_any_metadata(
- &mut self,
- visit_closers: bool,
- match_offset: i64,
- ) -> Option {
- if !self.next_tag_any(visit_closers, match_offset) {
- return None;
- }
-
- self.current_token_metadata()
- }
-
- pub fn next_tag_any_kind(&mut self, visit_closers: bool, match_offset: i64) -> i64 {
- if !self.next_tag_any(visit_closers, match_offset) {
- return 0;
- }
-
- if self.is_tag_closer() {
- 2
- } else {
- 1
- }
- }
-
- pub fn next_tag_any_kind_and_attribute_name_initials(
- &mut self,
- visit_closers: bool,
- match_offset: i64,
- ) -> i64 {
- if !self.next_tag_any(visit_closers, match_offset) {
- return 0;
- }
-
- let tag_kind = if self.is_tag_closer() { 2 } else { 1 };
- let attribute_name_initials = self
- .current_tag()
- .map(|tag| tag.attribute_name_initials as i64)
- .unwrap_or(0);
-
- tag_kind | (attribute_name_initials << 2)
- }
-
- pub fn next_tag_any_kind_and_attribute_name_initials_skip(&mut self) -> i64 {
- self.next_tag_any_kind_and_attribute_name_initials(false, 1)
- }
-
- pub fn next_tag_any_kind_and_attribute_name_initials_visit(&mut self) -> i64 {
- self.next_tag_any_kind_and_attribute_name_initials(true, 1)
- }
-
- pub fn next_tag_compact_summary_batch(
- &mut self,
- mut max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- if max_tags <= 0 {
- return None;
- }
-
- let mut rows = String::new();
-
- while max_tags > 0 && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- if !rows.is_empty() {
- rows.push('\x1e');
- }
-
- rows.push_str(&tag.name.to_ascii_uppercase());
- rows.push('\x1f');
- rows.push(if tag.closing { '1' } else { '0' });
-
- max_tags -= 1;
- }
-
- if rows.is_empty() {
- None
- } else {
- Some(rows)
- }
- }
-
- pub fn next_tag_summary_batch(
- &mut self,
- max_tags: i64,
- visit_closers: bool,
- ) -> Vec> {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- return Vec::new();
- };
- let mut rows = Vec::new();
-
- while rows.len() < limit && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- rows.push(html_tag_public_summary_row(tag));
- }
-
- rows
- }
-
- pub fn summarize_tag_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut open_tag_count = 0i64;
- let mut closing_tag_count = 0i64;
- let mut attribute_count = 0i64;
- let mut tag_names: std::collections::HashSet = std::collections::HashSet::new();
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
- tag_names.insert(tag.name.clone());
-
- if tag.closing {
- closing_tag_count += 1;
- } else {
- open_tag_count += 1;
- attribute_count += find_html_attribute_names_with_prefix_count(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "",
- )
- .unwrap_or(0);
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- open_tag_count,
- closing_tag_count,
- attribute_count,
- tag_names.len()
- )
- }
-
- pub fn summarize_heading_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut heading_count = 0i64;
- let mut h1_count = 0i64;
- let mut h2_count = 0i64;
- let mut h3_count = 0i64;
- let mut h4_count = 0i64;
- let mut h5_count = 0i64;
- let mut h6_count = 0i64;
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing {
- continue;
- }
-
- match tag.name.as_str() {
- "h1" => {
- heading_count += 1;
- h1_count += 1;
- }
- "h2" => {
- heading_count += 1;
- h2_count += 1;
- }
- "h3" => {
- heading_count += 1;
- h3_count += 1;
- }
- "h4" => {
- heading_count += 1;
- h4_count += 1;
- }
- "h5" => {
- heading_count += 1;
- h5_count += 1;
- }
- "h6" => {
- heading_count += 1;
- h6_count += 1;
- }
- _ => {}
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count, heading_count, h1_count, h2_count, h3_count, h4_count, h5_count, h6_count
- )
- }
-
- pub fn summarize_id_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut id_tag_count = 0i64;
- let mut duplicate_id_count = 0i64;
- let mut id_value_bytes = 0i64;
- let mut ids: std::collections::HashSet = std::collections::HashSet::new();
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing || 0 == tag.attribute_name_initials {
- continue;
- }
-
- let Some(has_value) = find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "id",
- ) else {
- continue;
- };
-
- id_tag_count += 1;
-
- if has_value {
- let id = find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "id",
- )
- .unwrap_or_default();
-
- id_value_bytes += id.len() as i64;
- if !ids.insert(id) {
- duplicate_id_count += 1;
- }
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- id_tag_count,
- ids.len(),
- duplicate_id_count,
- id_value_bytes
- )
- }
-
- pub fn summarize_attribute_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut attribute_count = 0i64;
- let mut attribute_value_bytes = 0i64;
- let mut attribute_names: std::collections::HashSet =
- std::collections::HashSet::new();
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing || 0 == tag.attribute_name_initials {
- continue;
- }
-
- let Some(current_attribute_names) = find_html_attribute_names_with_prefix(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "",
- ) else {
- continue;
- };
-
- for attribute_name in current_attribute_names {
- attribute_count += 1;
- attribute_names.insert(attribute_name.clone());
-
- if find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_name,
- )
- .unwrap_or(false)
- {
- attribute_value_bytes += find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_name,
- )
- .map(|value| value.len() as i64)
- .unwrap_or(0);
- }
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- attribute_count,
- attribute_names.len(),
- attribute_value_bytes
- )
- }
-
- pub fn summarize_data_attribute_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut data_attribute_tag_count = 0i64;
- let mut data_attribute_count = 0i64;
- let mut data_attribute_value_bytes = 0i64;
- let mut data_attribute_names: std::collections::HashSet =
- std::collections::HashSet::new();
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing || 0 == tag.attribute_name_initials {
- continue;
- }
-
- let Some(current_attribute_names) = find_html_attribute_names_with_prefix(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "data-",
- ) else {
- continue;
- };
-
- if current_attribute_names.is_empty() {
- continue;
- }
-
- data_attribute_tag_count += 1;
- for attribute_name in current_attribute_names {
- data_attribute_count += 1;
- data_attribute_names.insert(attribute_name.clone());
-
- if find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_name,
- )
- .unwrap_or(false)
- {
- data_attribute_value_bytes += find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_name,
- )
- .map(|value| value.len() as i64)
- .unwrap_or(0);
- }
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- data_attribute_tag_count,
- data_attribute_count,
- data_attribute_names.len(),
- data_attribute_value_bytes
- )
- }
-
- pub fn summarize_aria_attribute_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut aria_attribute_tag_count = 0i64;
- let mut aria_attribute_count = 0i64;
- let mut aria_attribute_value_bytes = 0i64;
- let mut aria_attribute_names: std::collections::HashSet =
- std::collections::HashSet::new();
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing || 0 == tag.attribute_name_initials {
- continue;
- }
-
- let Some(current_attribute_names) = find_html_attribute_names_with_prefix(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "aria-",
- ) else {
- continue;
- };
-
- if current_attribute_names.is_empty() {
- continue;
- }
-
- aria_attribute_tag_count += 1;
- for attribute_name in current_attribute_names {
- aria_attribute_count += 1;
- aria_attribute_names.insert(attribute_name.clone());
-
- if find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_name,
- )
- .unwrap_or(false)
- {
- aria_attribute_value_bytes += find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_name,
- )
- .map(|value| value.len() as i64)
- .unwrap_or(0);
- }
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- aria_attribute_tag_count,
- aria_attribute_count,
- aria_attribute_names.len(),
- aria_attribute_value_bytes
- )
- }
-
- pub fn summarize_class_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut class_attribute_count = 0i64;
- let mut class_name_count = 0i64;
- let mut class_value_bytes = 0i64;
- let mut class_names: std::collections::HashSet = std::collections::HashSet::new();
- let class_initial_bit = 1u32 << (b'c' - b'a');
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing || 0 == (tag.attribute_name_initials & class_initial_bit) {
- continue;
- }
-
- let has_class = find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "class",
- );
- let Some(has_value) = has_class else {
- continue;
- };
-
- class_attribute_count += 1;
-
- let class_value = if has_value {
- find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "class",
- )
- .unwrap_or_default()
- } else {
- String::new()
- };
- class_value_bytes += class_value.len() as i64;
-
- let mut tag_class_names: Vec = Vec::new();
- for class_name in class_value.split(['\t', '\n', '\x0c', '\r', ' ']) {
- if class_name.is_empty() {
- continue;
- }
-
- let normalized = class_name.replace('\0', "\u{fffd}");
- if tag_class_names.iter().any(|seen| seen == &normalized) {
- continue;
- }
-
- class_name_count += 1;
- class_names.insert(normalized.clone());
- tag_class_names.push(normalized);
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- class_attribute_count,
- class_name_count,
- class_names.len(),
- class_value_bytes
- )
- }
-
- pub fn summarize_resource_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut resource_tag_count = 0i64;
- let mut resource_attribute_count = 0i64;
- let mut resource_value_bytes = 0i64;
- let mut resource_tag_names: std::collections::HashSet =
- std::collections::HashSet::new();
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing {
- continue;
- }
-
- let attribute_name = match tag.name.as_str() {
- "a" | "link" => "href",
- "img" | "script" | "source" => "src",
- _ => continue,
- };
-
- let has_attribute = find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- attribute_name,
- );
- let Some(has_value) = has_attribute else {
- continue;
- };
-
- resource_tag_count += 1;
- resource_attribute_count += 1;
- resource_tag_names.insert(tag.name.clone());
-
- if has_value {
- resource_value_bytes += find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- attribute_name,
- )
- .map(|value| value.len() as i64)
- .unwrap_or(0);
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- resource_tag_count,
- resource_attribute_count,
- resource_tag_names.len(),
- resource_value_bytes
- )
- }
-
- pub fn summarize_image_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut image_count = 0i64;
- let mut src_count = 0i64;
- let mut alt_count = 0i64;
- let mut empty_alt_count = 0i64;
- let mut dimension_count = 0i64;
- let mut src_value_bytes = 0i64;
- let mut alt_value_bytes = 0i64;
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing || tag.name != "img" {
- continue;
- }
-
- image_count += 1;
-
- if let Some(has_value) = find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "src",
- ) {
- src_count += 1;
- if has_value {
- src_value_bytes += find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "src",
- )
- .map(|value| value.len() as i64)
- .unwrap_or(0);
- }
- }
-
- if let Some(has_value) = find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "alt",
- ) {
- alt_count += 1;
- if has_value {
- let alt_value = find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "alt",
- )
- .unwrap_or_default();
- if alt_value.is_empty() {
- empty_alt_count += 1;
- }
- alt_value_bytes += alt_value.len() as i64;
- } else {
- empty_alt_count += 1;
- }
- }
-
- if find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "width",
- )
- .is_some()
- && find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "height",
- )
- .is_some()
- {
- dimension_count += 1;
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- image_count,
- src_count,
- alt_count,
- empty_alt_count,
- dimension_count,
- src_value_bytes,
- alt_value_bytes
- )
- }
-
- pub fn summarize_script_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut script_count = 0i64;
- let mut src_count = 0i64;
- let mut module_count = 0i64;
- let mut async_count = 0i64;
- let mut defer_count = 0i64;
- let mut inline_script_bytes = 0i64;
- let mut src_value_bytes = 0i64;
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing || tag.name != "script" {
- continue;
- }
-
- script_count += 1;
-
- if let Some(has_value) = find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "src",
- ) {
- src_count += 1;
- if has_value {
- src_value_bytes += find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "src",
- )
- .map(|value| value.len() as i64)
- .unwrap_or(0);
- }
- } else {
- inline_script_bytes += tag.text.len() as i64;
- }
-
- if Some(true)
- == find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "type",
- )
- && find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "type",
- )
- .map(|value| value.eq_ignore_ascii_case("module"))
- .unwrap_or(false)
- {
- module_count += 1;
- }
-
- if find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "async",
- )
- .is_some()
- {
- async_count += 1;
- }
-
- if find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "defer",
- )
- .is_some()
- {
- defer_count += 1;
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- script_count,
- src_count,
- module_count,
- async_count,
- defer_count,
- inline_script_bytes,
- src_value_bytes
- )
- }
-
- pub fn summarize_form_inventory(&mut self, visit_closers: bool) -> String {
- let mut tag_count = 0i64;
- let mut form_count = 0i64;
- let mut control_count = 0i64;
- let mut named_control_count = 0i64;
- let mut control_name_value_bytes = 0i64;
- let mut control_names: std::collections::HashSet = std::collections::HashSet::new();
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing {
- continue;
- }
-
- match tag.name.as_str() {
- "form" => {
- form_count += 1;
- continue;
- }
- "input" | "select" | "textarea" | "button" => {
- control_count += 1;
- }
- _ => continue,
- }
-
- let has_name = find_html_attribute_has_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "name",
- );
- let Some(has_value) = has_name else {
- continue;
- };
-
- named_control_count += 1;
-
- let name_value = if has_value {
- find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- "name",
- )
- .unwrap_or_default()
- } else {
- String::new()
- };
- control_name_value_bytes += name_value.len() as i64;
- control_names.insert(name_value);
- }
-
- format!(
- "{}\x1f{}\x1f{}\x1f{}\x1f{}\x1f{}",
- tag_count,
- form_count,
- control_count,
- named_control_count,
- control_names.len(),
- control_name_value_bytes
- )
- }
-
- pub fn next_matching_tag_compact_summary_batch(
- &mut self,
- tag_name: String,
- mut max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- if max_tags <= 0 {
- return None;
- }
-
- let tag_name = tag_name.to_ascii_lowercase();
- let mut rows = String::new();
-
- while max_tags > 0 && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.name != tag_name {
- continue;
- }
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- if !rows.is_empty() {
- rows.push('\x1e');
- }
-
- rows.push_str(&tag.name.to_ascii_uppercase());
- rows.push('\x1f');
- rows.push(if tag.closing { '1' } else { '0' });
-
- max_tags -= 1;
- }
-
- if rows.is_empty() {
- None
- } else {
- Some(rows)
- }
- }
-
- pub fn next_matching_tag_summary_batch(
- &mut self,
- tag_name: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Vec> {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- return Vec::new();
- };
- let tag_name = tag_name.to_ascii_lowercase();
- let mut rows = Vec::new();
-
- while rows.len() < limit && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.name != tag_name {
- continue;
- }
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- rows.push(html_tag_public_summary_row(tag));
- }
-
- rows
- }
-
- pub fn next_matching_tag_attribute_compact_summary_batch(
- &mut self,
- tag_name: String,
- attribute_name: String,
- mut max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- if max_tags <= 0 {
- return None;
- }
-
- let tag_name = tag_name.to_ascii_lowercase();
- let attribute_name = attribute_name.to_ascii_lowercase();
- let mut rows = String::new();
-
- while max_tags > 0 && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.name != tag_name {
- continue;
- }
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- let attribute_value = if tag.closing {
- None
- } else {
- tag.attributes.get(&attribute_name).cloned().or_else(|| {
- find_html_attribute_value(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_name,
- )
- })
- };
-
- if !rows.is_empty() {
- rows.push('\x1e');
- }
-
- rows.push_str(&tag.name.to_ascii_uppercase());
- rows.push('\x1f');
- rows.push(if tag.closing { '1' } else { '0' });
- rows.push('\x1f');
- match attribute_value {
- Some(value) => {
- rows.push('1');
- rows.push_str(&value);
- }
- None => rows.push('0'),
- }
-
- max_tags -= 1;
- }
-
- if rows.is_empty() {
- None
- } else {
- Some(rows)
- }
- }
-
- pub fn next_matching_tag_attribute_summary_batch(
- &mut self,
- tag_name: String,
- attribute_name: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Vec> {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- return Vec::new();
- };
- let tag_name = tag_name.to_ascii_lowercase();
- let attribute_name = attribute_name.to_ascii_lowercase();
- let mut rows = Vec::new();
-
- while rows.len() < limit && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.name != tag_name {
- continue;
- }
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- rows.push(html_matching_tag_attribute_public_summary_row(
- self.html.as_bytes(),
- tag,
- &attribute_name,
- ));
- }
-
- rows
- }
-
- pub fn next_matching_tag_attributes_compact_summary_batch(
- &mut self,
- tag_name: String,
- attribute_names: String,
- mut max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- if max_tags <= 0 {
- return None;
- }
-
- let tag_name = tag_name.to_ascii_lowercase();
- let attribute_names: Vec = attribute_names
- .split('\x1f')
- .filter(|name| !name.is_empty())
- .map(|name| name.to_ascii_lowercase())
- .collect();
- let attribute_initial_bits = html_attribute_names_initial_bits(&attribute_names);
- let mut rows = String::new();
-
- while max_tags > 0 && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.name != tag_name {
- continue;
- }
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- let attribute_values = if tag.closing
- || attribute_names.is_empty()
- || (attribute_initial_bits != 0
- && 0 == (tag.attribute_name_initials & attribute_initial_bits))
- {
- Vec::new()
- } else {
- find_html_attribute_values(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_names,
- )
- };
-
- if !rows.is_empty() {
- rows.push('\x1e');
- }
-
- rows.push_str(&tag.name.to_ascii_uppercase());
- rows.push('\x1f');
- rows.push(if tag.closing { '1' } else { '0' });
- for index in 0..attribute_names.len() {
- rows.push('\x1f');
- match attribute_values.get(index).and_then(|value| value.as_ref()) {
- Some(value) => {
- rows.push('1');
- rows.push_str(value);
- }
- None => rows.push('0'),
- }
- }
-
- max_tags -= 1;
- }
-
- if rows.is_empty() {
- None
- } else {
- Some(rows)
- }
- }
-
- pub fn next_matching_tag_attributes_summary_batch(
- &mut self,
- tag_name: String,
- attribute_names: Vec,
- max_tags: i64,
- visit_closers: bool,
- ) -> Vec> {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- return Vec::new();
- };
- let tag_name = tag_name.to_ascii_lowercase();
- let attribute_names: Vec = attribute_names
- .into_iter()
- .filter(|name| !name.is_empty())
- .map(|name| name.to_ascii_lowercase())
- .collect();
- let attribute_initial_bits = html_attribute_names_initial_bits(&attribute_names);
- let mut rows = Vec::new();
-
- while rows.len() < limit && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.name != tag_name {
- continue;
- }
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- rows.push(html_matching_tag_attributes_public_summary_row(
- self.html.as_bytes(),
- tag,
- &attribute_names,
- attribute_initial_bits,
- ));
- }
-
- rows
- }
-
- pub fn summarize_matching_tag_attributes(
- &mut self,
- tag_name: String,
- attribute_names: String,
- visit_closers: bool,
- ) -> String {
- let tag_name = tag_name.to_ascii_lowercase();
- let attribute_names: Vec = attribute_names
- .split('\x1f')
- .filter(|name| !name.is_empty())
- .map(|name| name.to_ascii_lowercase())
- .collect();
- let attribute_initial_bits = html_attribute_names_initial_bits(&attribute_names);
- let mut tag_count = 0i64;
- let mut attribute_count = 0i64;
- let mut attribute_value_bytes = 0i64;
-
- while self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.name != tag_name {
- continue;
- }
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
-
- if tag.closing
- || attribute_names.is_empty()
- || (attribute_initial_bits != 0
- && 0 == (tag.attribute_name_initials & attribute_initial_bits))
- {
- continue;
- }
-
- for value in find_html_attribute_values(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &attribute_names,
- )
- .into_iter()
- .flatten()
- {
- attribute_count += 1;
- attribute_value_bytes += value.len() as i64;
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}",
- tag_count, attribute_count, attribute_value_bytes
- )
- }
-
- pub fn next_token(&mut self) -> bool {
- self.advance_to_next_token()
- }
-
- pub fn current_token_metadata(&self) -> Option {
- self.current_tag().map(token_metadata)
- }
-
- pub fn get_tag(&self) -> Option {
- self.current_tag().and_then(|tag| {
- if tag.token_type == "#tag" {
- Some(tag.name.to_ascii_uppercase())
- } else if tag.token_type == "#comment"
- && tag.comment_type.as_deref() == Some("COMMENT_AS_PI_NODE_LOOKALIKE")
- {
- Some(tag.name.clone())
- } else {
- None
- }
- })
- }
-
- pub fn get_token_name(&self) -> Option {
- self.current_tag().map(|tag| {
- if tag.token_type == "#tag" {
- tag.name.to_ascii_uppercase()
- } else if tag.token_type == "#doctype" {
- tag.name.clone()
- } else {
- tag.token_type.clone()
- }
- })
- }
-
- pub fn get_token_type(&self) -> Option {
- self.current_tag().map(|tag| tag.token_type.clone())
- }
-
- pub fn get_doctype_info(&self) -> Option {
- html_doctype_info_zval(&self.html, self.current_tag()?)
- }
-
- pub fn is_tag_closer(&self) -> bool {
- self.current_tag().map(|tag| tag.closing).unwrap_or(false)
- }
-
- pub fn get_namespace(&self) -> String {
- self.parsing_namespace.clone()
- }
-
- pub fn change_parsing_namespace(&mut self, new_namespace: String) -> bool {
- if !matches!(new_namespace.as_str(), "html" | "math" | "svg") {
- return false;
- }
-
- self.parsing_namespace = new_namespace;
-
- true
- }
-
- pub fn get_qualified_tag_name(&self) -> Option {
- self.get_tag()
- }
-
- pub fn get_qualified_attribute_name(&self, name: String) -> Option {
- match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" => Some(name),
- _ => None,
- }
- }
-
- pub fn has_self_closing_flag(&self) -> bool {
- match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => {
- html_tag_has_self_closing_flag(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- )
- }
- _ => false,
- }
- }
-
- pub fn get_attribute(&self, name: String) -> Zval {
- let name = name.to_ascii_lowercase();
-
- match self.get_attribute_value(&name) {
- Some(HtmlAttributeValue::Boolean) => html_zval_bool(true),
- Some(HtmlAttributeValue::String(value)) => html_zval_string(&value),
- None => html_zval_null(),
- }
- }
-
- pub fn get_attribute_names_with_prefix(&self, prefix: String) -> Option> {
- let prefix = prefix.to_ascii_lowercase();
- match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => {
- find_html_attribute_names_with_prefix(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &prefix,
- )
- }
- _ => None,
- }
- }
-
- pub fn class_list(&self) -> Option> {
- let tag = self.current_tag()?;
- if tag.token_type != "#tag" || tag.closing {
- return None;
- }
-
- let class_attribute = match self.get_attribute_value("class")? {
- HtmlAttributeValue::Boolean => String::new(),
- HtmlAttributeValue::String(value) => value,
- };
- let mut classes = Vec::new();
- for class_name in class_attribute.split(['\t', '\n', '\x0c', '\r', ' ']) {
- if class_name.is_empty() {
- continue;
- }
-
- let normalized = class_name.replace('\0', "\u{fffd}");
- if !classes.iter().any(|seen| seen == &normalized) {
- classes.push(normalized);
- }
- }
-
- Some(classes)
- }
-
- pub fn has_class(&self, wanted_class: String) -> Option {
- let classes = self.class_list()?;
- Some(classes.iter().any(|class_name| class_name == &wanted_class))
- }
-
- pub fn get_attribute_names_with_prefix_string(&self, prefix: String) -> Option {
- let prefix = prefix.to_ascii_lowercase();
- match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => {
- find_html_attribute_names_with_prefix_string(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &prefix,
- )
- }
- _ => None,
- }
- }
-
- pub fn count_attribute_names_with_prefix(&self, prefix: String) -> Option {
- let prefix = prefix.to_ascii_lowercase();
- let prefix_initial_bit = html_attribute_prefix_initial_bit(&prefix);
- match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => {
- if prefix_initial_bit != 0
- && 0 == (tag.attribute_name_initials & prefix_initial_bit)
- {
- return Some(0);
- }
-
- find_html_attribute_names_with_prefix_count(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &prefix,
- )
- }
- _ => None,
- }
- }
-
- pub fn summarize_attribute_names_with_prefix(
- &mut self,
- prefix: String,
- visit_closers: bool,
- ) -> String {
- let prefix = prefix.to_ascii_lowercase();
- let prefix_initial_bit = html_attribute_prefix_initial_bit(&prefix);
- let mut tag_count = 0i64;
- let mut attr_count = 0i64;
-
- while self.advance_to_next_tag_token() {
- match self.current_tag() {
- Some(HtmlTag { closing: true, .. }) if visit_closers => {
- tag_count += 1;
- }
- Some(HtmlTag { closing: true, .. }) => {}
- Some(tag) => {
- tag_count += 1;
- if prefix_initial_bit == 0
- || 0 != (tag.attribute_name_initials & prefix_initial_bit)
- {
- attr_count += find_html_attribute_names_with_prefix_count(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &prefix,
- )
- .unwrap_or(0);
- }
- }
- None => {}
- }
- }
-
- format!("{}\x1f{}", tag_count, attr_count)
- }
-
- pub fn next_tag_prefix_summary_batch(
- &mut self,
- prefix: String,
- mut max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- if max_tags <= 0 {
- return None;
- }
-
- let prefix = prefix.to_ascii_lowercase();
- let prefix_initial_bit = html_attribute_prefix_initial_bit(&prefix);
- let mut rows = String::new();
-
- while max_tags > 0 && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- if !rows.is_empty() {
- rows.push('\x1e');
- }
-
- rows.push_str(&tag.name.to_ascii_uppercase());
- rows.push('\x1f');
- rows.push(if tag.closing { '1' } else { '0' });
- rows.push('\x1f');
-
- let count = if tag.closing
- || (prefix_initial_bit != 0
- && 0 == (tag.attribute_name_initials & prefix_initial_bit))
- {
- 0
- } else {
- find_html_attribute_names_with_prefix_count(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &prefix,
- )
- .unwrap_or(0)
- };
- rows.push_str(&count.to_string());
-
- max_tags -= 1;
- }
-
- if rows.is_empty() {
- None
- } else {
- Some(rows)
- }
- }
-
- pub fn next_tag_prefix_compact_summary_batch(
- &mut self,
- prefix: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- self.next_tag_prefix_summary_batch(prefix, max_tags, visit_closers)
- }
-
- pub fn next_tag_prefix_count_compact_batch(
- &mut self,
- prefix: String,
- mut max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- if max_tags <= 0 {
- return None;
- }
-
- let prefix = prefix.to_ascii_lowercase();
- let prefix_initial_bit = html_attribute_prefix_initial_bit(&prefix);
- let mut tag_count = 0i64;
- let mut attr_count = 0i64;
-
- while max_tags > 0 && self.advance_to_next_tag_token() {
- let Some(tag) = self.current_tag() else {
- continue;
- };
-
- if tag.closing && !visit_closers {
- continue;
- }
-
- tag_count += 1;
- if !tag.closing
- && (prefix_initial_bit == 0
- || 0 != (tag.attribute_name_initials & prefix_initial_bit))
- {
- attr_count += find_html_attribute_names_with_prefix_count(
- self.html.as_bytes(),
- tag.source_start,
- tag.source_end,
- &prefix,
- )
- .unwrap_or(0);
- }
- max_tags -= 1;
- }
-
- if tag_count == 0 {
- None
- } else {
- Some(format!("{}\x1f{}", tag_count, attr_count))
- }
- }
-
- pub fn remove_attribute(&mut self, name: String) -> bool {
- let name = name.to_ascii_lowercase();
- let (tag_start, tag_end) = match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => {
- (tag.source_start, tag.source_end)
- }
- _ => return false,
- };
- let removals =
- find_html_attribute_removals(self.html.as_bytes(), tag_start, tag_end, &name);
-
- if removals.is_empty() {
- let removed_pending_update = self.updated_attributes.iter().any(|updated| {
- updated.tag_start == tag_start && updated.tag_end == tag_end && updated.name == name
- });
- if removed_pending_update {
- self.updated_attributes.retain(|updated| {
- updated.tag_start != tag_start
- || updated.tag_end != tag_end
- || updated.name != name
- });
- let insertion_start = html_tag_name_end(self.html.as_bytes(), tag_start, tag_end);
- self.removals.retain(|removal| {
- removal.start != insertion_start
- || removal.length != 0
- || !html_attribute_insertion_matches_name(&removal.replacement, &name)
- });
- self.removed_attributes.push(HtmlRemovedAttribute {
- tag_start,
- tag_end,
- name,
- });
- }
-
- return false;
- }
-
- for (start, length) in removals {
- self.removals.push(HtmlTextRemoval {
- start,
- length,
- replacement: String::new(),
- });
- }
- self.updated_attributes.retain(|updated| {
- updated.tag_start != tag_start || updated.tag_end != tag_end || updated.name != name
- });
- self.removed_attributes.push(HtmlRemovedAttribute {
- tag_start,
- tag_end,
- name,
- });
-
- true
- }
-
- pub fn set_attribute(&mut self, name: String, value: &Zval) -> bool {
- let tag = match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => tag.clone(),
- _ => return false,
- };
- if !is_valid_html_attribute_name(&name) {
- return false;
- }
- if value.is_false() {
- return self.remove_attribute(name);
- }
-
- let comparable_name = name.to_ascii_lowercase();
- let updated_value = if value.is_true() {
- None
- } else {
- match value.coerce_to_string() {
- Some(value) => Some(value),
- None => return false,
- }
- };
- let replacement = match updated_value.as_ref() {
- Some(value) => format!("{name}=\"{}\"", html_escape_attribute_value(value)),
- None => name.clone(),
- };
- let bytes = self.html.as_bytes();
- let (start, length, replacement) = match find_html_attribute_span(
- bytes,
- tag.source_start,
- tag.source_end,
- &comparable_name,
- ) {
- Some((start, length)) => (start, length, replacement),
- None => (
- html_tag_name_end(bytes, tag.source_start, tag.source_end),
- 0,
- format!(" {replacement}"),
- ),
- };
-
- if length == 0 {
- self.removals.retain(|removal| {
- removal.start != start
- || removal.length != 0
- || !html_attribute_insertion_matches_name(
- &removal.replacement,
- &comparable_name,
- )
- });
- }
-
- self.removals.push(HtmlTextRemoval {
- start,
- length,
- replacement,
- });
- self.removed_attributes.retain(|removed| {
- removed.tag_start != tag.source_start
- || removed.tag_end != tag.source_end
- || removed.name != comparable_name
- });
- self.updated_attributes.push(HtmlUpdatedAttribute {
- tag_start: tag.source_start,
- tag_end: tag.source_end,
- name: comparable_name,
- value: updated_value,
- });
-
- true
- }
-
- pub fn add_class(&mut self, class_name: String) -> bool {
- let tag = match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => tag.clone(),
- _ => return false,
- };
-
- let bytes = self.html.as_bytes();
- let original = tag.attributes.get("class").cloned().or_else(|| {
- find_html_attribute_value(bytes, tag.source_start, tag.source_end, "class")
- });
- let class_attribute_removed = self.removed_attributes.iter().any(|removed| {
- removed.tag_start == tag.source_start
- && removed.tag_end == tag.source_end
- && removed.name == "class"
- });
- let existing = self
- .updated_attributes
- .iter()
- .rev()
- .find(|updated| {
- updated.tag_start == tag.source_start
- && updated.tag_end == tag.source_end
- && updated.name == "class"
- })
- .map(|updated| updated.value.clone().unwrap_or_default())
- .or_else(|| {
- if class_attribute_removed {
- Some(String::new())
- } else {
- original.clone()
- }
- });
-
- let updated_value = match existing {
- Some(value) => {
- if value
- .split_ascii_whitespace()
- .any(|name| name == class_name)
- {
- return true;
- }
-
- if let Some(original_value) = original.as_deref() {
- if original_value
- .split_ascii_whitespace()
- .any(|name| name == class_name)
- {
- let current_classes: Vec<&str> = value.split_ascii_whitespace().collect();
- let mut restored_classes: Vec = Vec::new();
-
- for original_class in original_value.split_ascii_whitespace() {
- if original_class == class_name
- || current_classes.contains(&original_class)
- {
- restored_classes.push(original_class.to_string());
- }
- }
-
- for current_class in current_classes {
- if !restored_classes.iter().any(|name| name == current_class) {
- restored_classes.push(current_class.to_string());
- }
- }
-
- restored_classes.join(" ")
- } else if value.is_empty() {
- class_name.clone()
- } else {
- format!("{value} {class_name}")
- }
- } else if value.is_empty() {
- class_name.clone()
- } else {
- format!("{value} {class_name}")
- }
- }
- None => class_name.clone(),
- };
-
- let replacement = format!("class=\"{}\"", html_escape_attribute_value(&updated_value));
- let (start, length, replacement) =
- match find_html_attribute_span(bytes, tag.source_start, tag.source_end, "class") {
- Some((start, length)) => (start, length, replacement),
- None => (
- html_tag_name_end(bytes, tag.source_start, tag.source_end),
- 0,
- format!(" {replacement}"),
- ),
- };
-
- if length == 0 {
- self.removals.retain(|removal| {
- removal.start != start
- || removal.length != 0
- || !html_attribute_insertion_matches_name(&removal.replacement, "class")
- });
- }
-
- self.removals.push(HtmlTextRemoval {
- start,
- length,
- replacement,
- });
- self.removed_attributes.retain(|removed| {
- removed.tag_start != tag.source_start
- || removed.tag_end != tag.source_end
- || removed.name != "class"
- });
- self.updated_attributes.push(HtmlUpdatedAttribute {
- tag_start: tag.source_start,
- tag_end: tag.source_end,
- name: "class".to_string(),
- value: Some(updated_value),
- });
-
- true
- }
-
- pub fn remove_class(&mut self, class_name: String) -> bool {
- let tag = match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => tag.clone(),
- _ => return false,
- };
-
- let bytes = self.html.as_bytes();
- let existing = self
- .updated_attributes
- .iter()
- .rev()
- .find(|updated| {
- updated.tag_start == tag.source_start
- && updated.tag_end == tag.source_end
- && updated.name == "class"
- })
- .map(|updated| updated.value.clone().unwrap_or_default())
- .or_else(|| {
- tag.attributes.get("class").cloned().or_else(|| {
- find_html_attribute_value(bytes, tag.source_start, tag.source_end, "class")
- })
- });
- let value = match existing {
- Some(value) => value,
- None => return true,
- };
-
- let remaining: Vec<&str> = value
- .split_ascii_whitespace()
- .filter(|name| *name != class_name)
- .collect();
- let updated_value = remaining.join(" ");
- if updated_value == value {
- return true;
- }
-
- let removes_class_attribute = updated_value.is_empty();
- let replacement = if removes_class_attribute {
- String::new()
- } else {
- format!("class=\"{}\"", html_escape_attribute_value(&updated_value))
- };
- let (start, length, replacement) =
- match find_html_attribute_span(bytes, tag.source_start, tag.source_end, "class") {
- Some((start, length)) => (start, length, replacement),
- None => (
- html_tag_name_end(bytes, tag.source_start, tag.source_end),
- 0,
- if replacement.is_empty() {
- replacement
- } else {
- format!(" {replacement}")
- },
- ),
- };
-
- if length == 0 {
- self.removals.retain(|removal| {
- removal.start != start
- || removal.length != 0
- || !html_attribute_insertion_matches_name(&removal.replacement, "class")
- });
- }
-
- self.removals.push(HtmlTextRemoval {
- start,
- length,
- replacement,
- });
- self.updated_attributes.retain(|updated| {
- updated.tag_start != tag.source_start
- || updated.tag_end != tag.source_end
- || updated.name != "class"
- });
- if removes_class_attribute {
- self.removed_attributes.push(HtmlRemovedAttribute {
- tag_start: tag.source_start,
- tag_end: tag.source_end,
- name: "class".to_string(),
- });
- } else {
- self.updated_attributes.push(HtmlUpdatedAttribute {
- tag_start: tag.source_start,
- tag_end: tag.source_end,
- name: "class".to_string(),
- value: Some(updated_value),
- });
- }
-
- true
- }
-
- pub fn remove_attributes_with_prefix(&mut self, prefix: String) -> Option {
- let prefix = prefix.to_ascii_lowercase();
- let (tag_start, tag_end) = match self.current_tag() {
- Some(tag) if tag.token_type == "#tag" && !tag.closing => {
- (tag.source_start, tag.source_end)
- }
- _ => return None,
- };
- let removed_names: Vec = self
- .removed_attributes
- .iter()
- .filter(|removed| removed.tag_start == tag_start && removed.tag_end == tag_end)
- .map(|removed| removed.name.clone())
- .collect();
-
- let (removed_count, removals, names) = find_html_attribute_removals_with_prefix(
- self.html.as_bytes(),
- tag_start,
- tag_end,
- &prefix,
- &removed_names,
- )?;
-
- for (start, length) in removals {
- self.removals.push(HtmlTextRemoval {
- start,
- length,
- replacement: String::new(),
- });
- }
- for name in names {
- self.removed_attributes.push(HtmlRemovedAttribute {
- tag_start,
- tag_end,
- name,
- });
- }
-
- Some(removed_count)
- }
-
- pub fn remove_attributes_with_prefix_from_document(
- &mut self,
- prefix: String,
- visit_closers: bool,
- ) -> String {
- let prefix = prefix.to_ascii_lowercase();
- let mut tag_count = 0i64;
- let mut removed_count = 0i64;
-
- while self.advance_to_next_tag_token() {
- let (tag_start, tag_end, is_closing) = match self.current_tag() {
- Some(tag) => (tag.source_start, tag.source_end, tag.closing),
- None => continue,
- };
-
- if is_closing {
- if visit_closers {
- tag_count += 1;
- }
- continue;
- }
-
- tag_count += 1;
-
- let removed_names: Vec = self
- .removed_attributes
- .iter()
- .filter(|removed| removed.tag_start == tag_start && removed.tag_end == tag_end)
- .map(|removed| removed.name.clone())
- .collect();
-
- let Some((count, removals, names)) = find_html_attribute_removals_with_prefix(
- self.html.as_bytes(),
- tag_start,
- tag_end,
- &prefix,
- &removed_names,
- ) else {
- continue;
- };
-
- removed_count += count;
- for (start, length) in removals {
- self.removals.push(HtmlTextRemoval {
- start,
- length,
- replacement: String::new(),
- });
- }
- for name in names {
- self.removed_attributes.push(HtmlRemovedAttribute {
- tag_start,
- tag_end,
- name,
- });
- }
- }
-
- format!(
- "{}\x1f{}\x1f{}",
- tag_count,
- removed_count,
- self.get_updated_html()
- )
- }
-
- pub fn get_updated_html(&self) -> String {
- if self.removals.is_empty() {
- return self.html.clone();
- }
-
- apply_html_text_removals(&self.html, &self.removals)
- }
-
- #[php(name = "__toString")]
- pub fn __to_string(&self) -> String {
- self.get_updated_html()
- }
-
- pub fn set_bookmark(&mut self, name: String) -> bool {
- if self.current.is_none() {
- return false;
- }
-
- self.bookmarks.insert(
- name,
- HtmlBookmark {
- offset: self.offset,
- breadcrumbs: self.breadcrumbs.clone(),
- parsing_namespace: self.parsing_namespace.clone(),
- synthesize_implied_closers: self.synthesize_implied_closers,
- ignore_html_body_starts: self.ignore_html_body_starts,
- current: self.current.clone(),
- },
- );
-
- true
- }
-
- pub fn release_bookmark(&mut self, name: String) -> bool {
- self.bookmarks.remove(&name).is_some()
- }
-
- pub fn has_bookmark(&self, name: String) -> bool {
- self.bookmarks.contains_key(&name)
- }
-
- pub fn seek(&mut self, name: String) -> bool {
- let Some(bookmark) = self.bookmarks.get(&name).cloned() else {
- return false;
- };
-
- self.offset = bookmark.offset;
- self.breadcrumbs = bookmark.breadcrumbs;
- self.parsing_namespace = bookmark.parsing_namespace;
- self.synthesize_implied_closers = bookmark.synthesize_implied_closers;
- self.ignore_html_body_starts = bookmark.ignore_html_body_starts;
- self.current = bookmark.current;
-
- true
- }
-
- pub fn get_modifiable_text(&self) -> String {
- self.current_tag()
- .map(|tag| tag.text.clone())
- .unwrap_or_default()
- }
-
- pub fn set_modifiable_text(&mut self, plaintext_content: String) -> bool {
- let Some(tag) = self.current.as_mut() else {
- return false;
- };
-
- let (start, end, replacement) = match tag.token_type.as_str() {
- "#text" => (
- tag.source_start,
- tag.source_end,
- html_escape_text(&plaintext_content),
- ),
- "#comment" if tag.comment_type.as_deref() == Some("COMMENT_AS_HTML_COMMENT") => {
- if plaintext_content.contains("-->") || plaintext_content.contains("--!>") {
- return false;
- }
-
- (
- tag.source_start.saturating_add(4),
- tag.source_end.saturating_sub(3),
- plaintext_content.clone(),
- )
- }
- _ => return false,
- };
-
- if end < start || end > self.html.len() {
- return false;
- }
-
- self.removals.push(HtmlTextRemoval {
- start,
- length: end - start,
- replacement,
- });
- tag.text = plaintext_content.clone();
- if tag.token_type == "#comment" {
- tag.full_comment_text = Some(plaintext_content);
- }
-
- true
- }
-
- pub fn subdivide_text_appropriately(&mut self) -> bool {
- let Some(tag) = self.current.as_mut() else {
- return false;
- };
-
- if tag.token_type != "#text" {
- return false;
- }
-
- let bytes = self.html.as_bytes();
- let start = tag.source_start;
- let end = tag.source_end.min(bytes.len());
- if start >= end {
- return false;
- }
-
- let mut split_at = start;
- if bytes[start] == b'\0' {
- while split_at < end && bytes[split_at] == b'\0' {
- split_at += 1;
- }
-
- tag.text.clear();
- } else {
- while split_at < end && is_html_ascii_whitespace(bytes[split_at]) {
- split_at += 1;
- }
-
- if split_at == start {
- return false;
- }
-
- tag.text = String::from_utf8_lossy(&bytes[start..split_at]).into_owned();
- normalize_html_newlines_in_place(&mut tag.text);
- }
-
- tag.source_end = split_at;
- self.offset = split_at;
-
- true
- }
-
- pub fn paused_at_incomplete_token(&self) -> bool {
- false
- }
-
- pub fn get_comment_type(&self) -> Option {
- self.current_tag().and_then(|tag| tag.comment_type.clone())
- }
-
- pub fn get_full_comment_text(&self) -> Option {
- self.current_tag().and_then(|tag| {
- if matches!(tag.token_type.as_str(), "#comment" | "#funky-comment") {
- tag.full_comment_text.clone()
- } else {
- None
- }
- })
- }
-}
-
-#[cfg(feature = "php-extension")]
-impl WpHtmlNativeTagProcessor {
- fn advance_to_next_token(&mut self) -> bool {
- self.current = parse_next_html_token(
- self.html.as_bytes(),
- &mut self.offset,
- &mut self.breadcrumbs,
- true,
- false,
- self.synthesize_implied_closers,
- self.ignore_html_body_starts,
- );
- self.current.is_some()
- }
-
- fn advance_to_next_tag_token(&mut self) -> bool {
- if !self.synthesize_implied_closers && !self.ignore_html_body_starts {
- self.current = parse_next_plain_html_tag_token(self.html.as_bytes(), &mut self.offset);
-
- return self.current.is_some();
- }
-
- loop {
- self.current = parse_next_html_token(
- self.html.as_bytes(),
- &mut self.offset,
- &mut self.breadcrumbs,
- false,
- false,
- self.synthesize_implied_closers,
- self.ignore_html_body_starts,
- );
-
- match self.current.as_ref() {
- Some(tag) if tag.token_type == "#tag" => return true,
- Some(_) => continue,
- None => return false,
- }
- }
- }
-
- fn current_tag(&self) -> Option<&HtmlTag> {
- self.current.as_ref()
- }
-
- fn current_html_tag_matches_query(
- &self,
- query: &HtmlNextTagQuery,
- use_breadcrumbs: bool,
- ) -> bool {
- let Some(tag) = self.current_tag() else {
- return false;
- };
-
- if tag.token_type != "#tag" {
- return false;
- }
-
- if tag.closing && (!query.visit_closers || use_breadcrumbs) {
- return false;
- }
-
- if let Some(tag_name) = query.tag_name.as_ref() {
- if !tag.name.eq_ignore_ascii_case(tag_name) {
- return false;
- }
- }
-
- if let Some(class_name) = query.class_name.as_ref() {
- if self.has_class(class_name.clone()) != Some(true) {
- return false;
- }
- }
-
- if use_breadcrumbs {
- if let Some(breadcrumbs) = query.breadcrumbs.as_ref() {
- return html_breadcrumbs_match(&tag.breadcrumbs, breadcrumbs);
- }
- }
-
- true
- }
-}
-
-#[cfg(feature = "php-extension")]
-#[php_class]
-#[php(name = "WP_HTML_Native_Processor")]
-pub struct WpHtmlNativeProcessor {
- inner: WpHtmlNativeTagProcessor,
-}
-
-#[cfg(feature = "php-extension")]
-#[php_impl]
-#[php(change_method_case = "snake_case")]
-impl WpHtmlNativeProcessor {
- pub fn supports_public_api() -> bool {
- true
- }
-
- #[php(optional = context)]
- pub fn create_fragment(
- html: String,
- context: Option,
- encoding: Option,
- ) -> Option {
- if context.as_deref().unwrap_or("") != ""
- || encoding.as_deref().unwrap_or("UTF-8") != "UTF-8"
- {
- return None;
- }
-
- let mut inner = WpHtmlNativeTagProcessor::__construct(html);
- inner.synthesize_implied_closers = true;
- inner.ignore_html_body_starts = true;
-
- Some(Self { inner })
- }
-
- #[php(optional = known_definite_encoding)]
- pub fn create_full_parser(
- html: String,
- known_definite_encoding: Option,
- ) -> Option {
- if known_definite_encoding.unwrap_or_else(|| "UTF-8".to_string()) != "UTF-8" {
- return None;
- }
-
- let mut inner = WpHtmlNativeTagProcessor::__construct(html);
- inner.synthesize_implied_closers = true;
-
- Some(Self { inner })
- }
-
- pub fn normalize(html: String) -> Option {
- html_serialize_native_fragment(&html)
- }
-
- pub fn serialize(&mut self) -> Option {
- if self.inner.offset != 0 || self.inner.current.is_some() {
- return None;
- }
-
- let serialized = html_serialize_native_fragment(&self.inner.html)?;
- self.inner.offset = self.inner.html.len();
- Some(serialized)
- }
-
- pub fn is_void(tag_name: String) -> bool {
- is_html_processor_void_element(&tag_name.to_ascii_lowercase())
- }
-
- pub fn is_special(tag_name: String) -> bool {
- is_html_processor_special_element(&tag_name.to_ascii_lowercase())
- }
-
- pub fn next_token(&mut self) -> bool {
- while self.inner.advance_to_next_token() {
- if self.inner.get_token_type().as_deref() != Some("#doctype") {
- return true;
- }
- }
-
- false
- }
-
- #[php(optional = node_to_process)]
- pub fn step(&mut self, node_to_process: Option) -> bool {
- match node_to_process.as_deref().unwrap_or("process-next-node") {
- "process-next-node" => self.next_token(),
- "reprocess-current-node" => self.inner.current_tag().is_some(),
- _ => false,
- }
- }
-
- pub fn get_doctype_info(&self) -> Option {
- self.inner.get_doctype_info()
- }
-
- pub fn next_token_metadata(&mut self) -> Option {
- if !self.next_token() {
- return None;
- }
-
- self.current_token_metadata()
- }
-
- pub fn next_token_compact_summary_batch(&mut self, mut max_tokens: i64) -> Option {
- if max_tokens <= 0 {
- return None;
- }
-
- let mut rows = String::new();
- while max_tokens > 0 && self.next_token() {
- let Some(token) = self.inner.current_tag() else {
- continue;
- };
-
- if !rows.is_empty() {
- rows.push('\x1e');
- }
-
- rows.push_str(&html_token_compact_summary(token));
- max_tokens -= 1;
- }
-
- if rows.is_empty() {
- None
- } else {
- Some(rows)
- }
- }
-
- pub fn next_token_summary_batch(&mut self, max_tokens: i64) -> Vec> {
- let limit = if max_tokens > 0 {
- max_tokens.min(256) as usize
- } else {
- return Vec::new();
- };
- let mut rows = Vec::new();
-
- while rows.len() < limit && self.next_token() {
- let Some(token) = self.inner.current_tag() else {
- continue;
- };
-
- rows.push(html_token_public_summary_row(token));
- }
-
- rows
- }
-
- #[php(optional = query)]
- pub fn next_tag(&mut self, query: Option<&Zval>) -> bool {
- let Some(query) = html_parse_processor_next_tag_query(query) else {
- return false;
- };
- let use_breadcrumbs = query.breadcrumbs.is_some();
- let mut match_offset = query.match_offset.max(1);
-
- while self.next_token() {
- if !self
- .inner
- .current_html_tag_matches_query(&query, use_breadcrumbs)
- {
- continue;
- }
-
- match_offset -= 1;
- if match_offset == 0 {
- return true;
- }
- }
-
- false
- }
-
- pub fn next_tag_summary_batch(
- &mut self,
- max_tags: i64,
- visit_closers: bool,
- ) -> Vec> {
- self.inner.next_tag_summary_batch(max_tags, visit_closers)
- }
-
- pub fn next_tag_compact_summary_batch(
- &mut self,
- max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- self.inner
- .next_tag_compact_summary_batch(max_tags, visit_closers)
- }
-
- pub fn next_matching_tag_compact_summary_batch(
- &mut self,
- tag_name: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- self.inner
- .next_matching_tag_compact_summary_batch(tag_name, max_tags, visit_closers)
- }
-
- pub fn next_matching_tag_summary_batch(
- &mut self,
- tag_name: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Vec> {
- self.inner
- .next_matching_tag_summary_batch(tag_name, max_tags, visit_closers)
- }
-
- pub fn next_matching_tag_attribute_compact_summary_batch(
- &mut self,
- tag_name: String,
- attribute_name: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- self.inner
- .next_matching_tag_attribute_compact_summary_batch(
- tag_name,
- attribute_name,
- max_tags,
- visit_closers,
- )
- }
-
- pub fn next_matching_tag_attribute_summary_batch(
- &mut self,
- tag_name: String,
- attribute_name: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Vec> {
- self.inner.next_matching_tag_attribute_summary_batch(
- tag_name,
- attribute_name,
- max_tags,
- visit_closers,
- )
- }
-
- pub fn next_matching_tag_attributes_compact_summary_batch(
- &mut self,
- tag_name: String,
- attribute_names: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- self.inner
- .next_matching_tag_attributes_compact_summary_batch(
- tag_name,
- attribute_names,
- max_tags,
- visit_closers,
- )
- }
-
- pub fn next_matching_tag_attributes_summary_batch(
- &mut self,
- tag_name: String,
- attribute_names: Vec,
- max_tags: i64,
- visit_closers: bool,
- ) -> Vec> {
- self.inner.next_matching_tag_attributes_summary_batch(
- tag_name,
- attribute_names,
- max_tags,
- visit_closers,
- )
- }
-
- pub fn summarize_matching_tag_attributes(
- &mut self,
- tag_name: String,
- attribute_names: String,
- visit_closers: bool,
- ) -> String {
- self.inner
- .summarize_matching_tag_attributes(tag_name, attribute_names, visit_closers)
- }
-
- pub fn current_token_metadata(&self) -> Option {
- self.inner.current_tag().map(token_metadata)
- }
-
- pub fn get_token_name(&self) -> Option {
- self.inner.get_token_name()
- }
-
- pub fn get_tag(&self) -> Option {
- self.inner.get_tag()
- }
-
- pub fn get_token_type(&self) -> Option {
- self.inner.get_token_type()
- }
-
- pub fn is_tag_closer(&self) -> bool {
- self.inner.is_tag_closer()
- }
-
- pub fn is_virtual(&self) -> bool {
- false
- }
-
- pub fn expects_closer(&self) -> Option {
- let token = self.inner.current_tag()?;
-
- if token.token_type != "#tag" {
- return Some(false);
- }
-
- Some(
- !is_html_processor_void_element(&token.name)
- && !is_html_raw_text_element(&token.name)
- && !self.has_self_closing_flag(),
- )
- }
-
- pub fn get_last_error(&self) -> Option {
- None
- }
-
- pub fn get_unsupported_exception(&self) -> Option {
- None
- }
-
- pub fn get_namespace(&self) -> String {
- self.inner.get_namespace()
- }
-
- pub fn change_parsing_namespace(&mut self, new_namespace: String) -> bool {
- self.inner.change_parsing_namespace(new_namespace)
- }
-
- pub fn get_qualified_tag_name(&self) -> Option {
- self.inner.get_qualified_tag_name()
- }
-
- pub fn get_qualified_attribute_name(&self, name: String) -> Option {
- self.inner.get_qualified_attribute_name(name)
- }
-
- pub fn has_self_closing_flag(&self) -> bool {
- self.inner.has_self_closing_flag()
- }
-
- pub fn paused_at_incomplete_token(&self) -> bool {
- self.inner.paused_at_incomplete_token()
- }
-
- pub fn get_attribute(&self, name: String) -> Zval {
- self.inner.get_attribute(name)
- }
-
- pub fn get_attribute_names_with_prefix(&self, prefix: String) -> Option> {
- self.inner.get_attribute_names_with_prefix(prefix)
- }
-
- pub fn count_attribute_names_with_prefix(&self, prefix: String) -> Option {
- self.inner.count_attribute_names_with_prefix(prefix)
- }
-
- pub fn summarize_attribute_names_with_prefix(
- &mut self,
- prefix: String,
- visit_closers: bool,
- ) -> String {
- self.inner
- .summarize_attribute_names_with_prefix(prefix, visit_closers)
- }
-
- pub fn next_tag_prefix_summary_batch(
- &mut self,
- prefix: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- self.inner
- .next_tag_prefix_summary_batch(prefix, max_tags, visit_closers)
- }
-
- pub fn next_tag_prefix_compact_summary_batch(
- &mut self,
- prefix: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- self.inner
- .next_tag_prefix_compact_summary_batch(prefix, max_tags, visit_closers)
- }
-
- pub fn next_tag_prefix_count_compact_batch(
- &mut self,
- prefix: String,
- max_tags: i64,
- visit_closers: bool,
- ) -> Option {
- self.inner
- .next_tag_prefix_count_compact_batch(prefix, max_tags, visit_closers)
- }
-
- pub fn summarize_tag_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_tag_inventory(visit_closers)
- }
-
- pub fn summarize_heading_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_heading_inventory(visit_closers)
- }
-
- pub fn summarize_id_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_id_inventory(visit_closers)
- }
-
- pub fn summarize_attribute_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_attribute_inventory(visit_closers)
- }
-
- pub fn summarize_data_attribute_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_data_attribute_inventory(visit_closers)
- }
-
- pub fn summarize_aria_attribute_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_aria_attribute_inventory(visit_closers)
- }
-
- pub fn summarize_class_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_class_inventory(visit_closers)
- }
-
- pub fn summarize_resource_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_resource_inventory(visit_closers)
- }
-
- pub fn summarize_image_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_image_inventory(visit_closers)
- }
-
- pub fn summarize_script_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_script_inventory(visit_closers)
- }
-
- pub fn summarize_form_inventory(&mut self, visit_closers: bool) -> String {
- self.inner.summarize_form_inventory(visit_closers)
- }
-
- pub fn remove_attributes_with_prefix(&mut self, prefix: String) -> Option {
- self.inner.remove_attributes_with_prefix(prefix)
- }
-
- pub fn remove_attributes_with_prefix_from_document(
- &mut self,
- prefix: String,
- visit_closers: bool,
- ) -> String {
- self.inner
- .remove_attributes_with_prefix_from_document(prefix, visit_closers)
- }
-
- pub fn get_updated_html(&self) -> String {
- self.inner.get_updated_html()
- }
-
- #[php(name = "__toString")]
- pub fn __to_string(&self) -> String {
- self.get_updated_html()
- }
-
- pub fn class_list(&self) -> Option> {
- if self.is_virtual() {
- return None;
- }
-
- self.inner.class_list()
- }
-
- pub fn has_class(&self, wanted_class: String) -> Option {
- if self.is_virtual() {
- return None;
- }
-
- self.inner.has_class(wanted_class)
- }
-
- pub fn remove_attribute(&mut self, name: String) -> bool {
- self.inner.remove_attribute(name)
- }
-
- pub fn set_attribute(&mut self, name: String, value: &Zval) -> bool {
- if self.is_virtual() {
- return false;
- }
-
- self.inner.set_attribute(name, value)
- }
-
- pub fn add_class(&mut self, class_name: String) -> bool {
- if self.is_virtual() {
- return false;
- }
-
- self.inner.add_class(class_name)
- }
-
- pub fn remove_class(&mut self, class_name: String) -> bool {
- if self.is_virtual() {
- return false;
- }
-
- self.inner.remove_class(class_name)
- }
-
- pub fn set_bookmark(&mut self, name: String) -> bool {
- self.inner.set_bookmark(name)
- }
-
- pub fn release_bookmark(&mut self, name: String) -> bool {
- self.inner.release_bookmark(name)
- }
-
- pub fn has_bookmark(&self, name: String) -> bool {
- self.inner.has_bookmark(name)
- }
-
- pub fn seek(&mut self, name: String) -> bool {
- self.inner.seek(name)
- }
-
- pub fn get_modifiable_text(&self) -> String {
- self.inner
- .current_tag()
- .map(|tag| tag.text.clone())
- .unwrap_or_default()
- }
-
- pub fn set_modifiable_text(&mut self, plaintext_content: String) -> bool {
- self.inner.set_modifiable_text(plaintext_content)
- }
-
- pub fn subdivide_text_appropriately(&mut self) -> bool {
- self.inner.subdivide_text_appropriately()
- }
-
- pub fn get_comment_type(&self) -> Option {
- self.inner.get_comment_type()
- }
-
- pub fn get_full_comment_text(&self) -> Option {
- self.inner.get_full_comment_text()
- }
-
- pub fn get_breadcrumbs(&self) -> Vec {
- self.inner
- .current_tag()
- .map(|tag| tag.breadcrumbs.clone())
- .unwrap_or_else(|| vec!["HTML".to_string(), "BODY".to_string()])
- }
-
- pub fn matches_breadcrumbs(&self, breadcrumbs: Vec) -> bool {
- if breadcrumbs.is_empty() {
- return true;
- }
-
- let tag = match self.inner.current_tag() {
- Some(tag) if tag.token_type == "#tag" => tag,
- _ => return false,
- };
-
- if breadcrumbs.len() > tag.breadcrumbs.len() {
- return false;
- }
-
- let offset = tag.breadcrumbs.len() - breadcrumbs.len();
- for (index, breadcrumb) in breadcrumbs.iter().enumerate() {
- let crumb = breadcrumb.to_ascii_uppercase();
- let node = &tag.breadcrumbs[offset + index];
- if crumb != "*" && node != &crumb {
- return false;
- }
- }
-
- true
- }
-
- pub fn get_current_depth(&self) -> i64 {
- self.inner
- .current_tag()
- .map(|tag| tag.depth as i64)
- .unwrap_or(2)
- }
-}
-
-#[cfg(feature = "php-extension")]
-fn token_metadata(tag: &HtmlTag) -> String {
- let token_name = if tag.token_type == "#tag" {
- tag.name.to_ascii_uppercase()
- } else if tag.token_type == "#doctype" {
- tag.name.clone()
- } else {
- tag.token_type.clone()
- };
- let closer = if tag.closing { "1" } else { "0" };
-
- let mut metadata =
- String::with_capacity(tag.token_type.len() + token_name.len() + tag.breadcrumbs.len() * 8);
- metadata.push_str(&tag.token_type);
- metadata.push('\x1f');
- metadata.push_str(&token_name);
- metadata.push('\x1f');
- metadata.push_str(closer);
- for breadcrumb in &tag.breadcrumbs {
- metadata.push('\x1f');
- metadata.push_str(breadcrumb);
- }
-
- metadata
-}
-
-#[cfg(feature = "php-extension")]
-fn html_parse_tag_processor_next_tag_query(query: Option<&Zval>) -> HtmlNextTagQuery {
- let mut parsed = HtmlNextTagQuery {
- match_offset: 1,
- ..Default::default()
- };
-
- let Some(query) = query else {
- return parsed;
- };
-
- if let Some(tag_name) = query.str() {
- parsed.tag_name = Some(tag_name.to_string());
- return parsed;
- }
-
- let Some(query) = query.array() else {
- return parsed;
- };
-
- if let Some(tag_name) = html_array_string(query, "tag_name") {
- parsed.tag_name = Some(tag_name);
- }
-
- if let Some(class_name) = html_array_string(query, "class_name") {
- parsed.class_name = Some(class_name);
- }
-
- if let Some(match_offset) = html_array_positive_i64(query, "match_offset") {
- parsed.match_offset = match_offset;
- }
-
- parsed.visit_closers = html_array_string(query, "tag_closers").as_deref() == Some("visit");
-
- parsed
-}
-
-#[cfg(feature = "php-extension")]
-fn html_parse_processor_next_tag_query(query: Option<&Zval>) -> Option {
- let mut parsed = HtmlNextTagQuery {
- match_offset: 1,
- ..Default::default()
- };
-
- let Some(query) = query else {
- return Some(parsed);
- };
-
- if let Some(tag_name) = query.str() {
- parsed.breadcrumbs = Some(vec![tag_name.to_string()]);
- return Some(parsed);
- }
-
- let query = query.array()?;
-
- if let Some(tag_name) = html_array_string(query, "tag_name") {
- parsed.tag_name = Some(tag_name);
- }
-
- if let Some(class_name) = html_array_string(query, "class_name") {
- parsed.class_name = Some(class_name);
- }
-
- if let Some(match_offset) = html_array_positive_i64(query, "match_offset") {
- parsed.match_offset = match_offset;
- }
-
- parsed.visit_closers = html_array_string(query, "tag_closers").as_deref() == Some("visit");
- parsed.breadcrumbs = html_array_string_breadcrumbs(query, "breadcrumbs");
-
- Some(parsed)
-}
-
-#[cfg(feature = "php-extension")]
-fn html_array_string(query: &ZendHashTable, key: &str) -> Option {
- query
- .get(key)
- .and_then(|value| value.str())
- .map(str::to_string)
-}
-
-#[cfg(feature = "php-extension")]
-fn html_array_positive_i64(query: &ZendHashTable, key: &str) -> Option {
- query
- .get(key)
- .and_then(|value| value.long())
- .filter(|value| *value > 0)
- .map(|value| value as i64)
-}
-
-#[cfg(feature = "php-extension")]
-fn html_array_string_breadcrumbs(query: &ZendHashTable, key: &str) -> Option> {
- let breadcrumbs = query.get(key)?.array()?;
- let mut parsed = Vec::with_capacity(breadcrumbs.len());
-
- for value in breadcrumbs.iter().map(|(_, value)| value) {
- parsed.push(value.str()?.to_string());
- }
-
- Some(parsed)
-}
-
-fn html_breadcrumbs_match(current: &[String], breadcrumbs: &[String]) -> bool {
- if breadcrumbs.is_empty() {
- return true;
- }
-
- if breadcrumbs.len() > current.len() {
- return false;
- }
-
- let offset = current.len() - breadcrumbs.len();
- for (index, breadcrumb) in breadcrumbs.iter().enumerate() {
- let crumb = breadcrumb.to_ascii_uppercase();
- let node = ¤t[offset + index];
- if crumb != "*" && node != &crumb {
- return false;
- }
- }
-
- true
-}
-
-#[cfg(feature = "php-extension")]
-fn html_serialize_native_fragment(html: &str) -> Option {
- let mut processor = WpHtmlNativeProcessor::create_fragment(
- html.to_string(),
- Some("".to_string()),
- Some("UTF-8".to_string()),
- )?;
- let mut serialized = String::with_capacity(html.len());
-
- while processor.next_token() {
- let Some(tag) = processor.inner.current_tag() else {
- continue;
- };
-
- serialized.push_str(&html_serialize_token(html, tag));
- }
-
- Some(serialized)
-}
-
-fn html_serialize_token(html: &str, tag: &HtmlTag) -> String {
- match tag.token_type.as_str() {
- "#tag" if tag.closing => format!("{}>", tag.name),
- "#tag" => html_serialize_opening_tag(html, tag),
- "#comment" => format!("", tag.text),
- "#text" => html_escape_text(&tag.text),
- _ => String::new(),
- }
-}
-
-fn html_serialize_opening_tag(html: &str, tag: &HtmlTag) -> String {
- let mut output = String::new();
- output.push('<');
- output.push_str(&tag.name);
-
- for (attribute_name, value) in
- html_source_attribute_items(html.as_bytes(), tag.source_start, tag.source_end)
- {
- output.push(' ');
- output.push_str(&attribute_name);
-
- if let Some(value) = value {
- output.push_str("=\"");
- output.push_str(&html_escape_attribute_value(&value));
- output.push('"');
- }
- }
-
- output.push('>');
- output
-}
-
-fn html_source_attribute_items(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
-) -> Vec<(String, Option)> {
- if source_start >= bytes.len() || source_end <= source_start {
- return Vec::new();
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return Vec::new();
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- let mut items = Vec::new();
- let mut seen = Vec::new();
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- let attr_name = ascii_lower(&bytes[attr_start..cursor]);
- cursor = skip_ascii_whitespace(bytes, cursor);
-
- let mut value = None;
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- let parsed = parse_attribute_value(bytes, cursor);
- value = Some(parsed.0);
- cursor = parsed.1;
- }
-
- if seen
- .iter()
- .any(|seen_name: &String| seen_name == &attr_name)
- {
- continue;
- }
-
- seen.push(attr_name.clone());
- items.push((attr_name, value));
- }
-
- items
-}
-
-#[cfg(feature = "php-extension")]
-fn html_tag_public_summary_row(tag: &HtmlTag) -> Vec<(String, Zval)> {
- vec![
- (
- "tag_name".to_string(),
- html_zval_string(&tag.name.to_ascii_uppercase()),
- ),
- ("is_tag_closer".to_string(), html_zval_bool(tag.closing)),
- ]
-}
-
-#[cfg(feature = "php-extension")]
-fn html_matching_tag_attribute_public_summary_row(
- html: &[u8],
- tag: &HtmlTag,
- attribute_name: &str,
-) -> Vec<(String, Zval)> {
- let attribute_value = if tag.closing {
- None
- } else {
- tag.attributes.get(attribute_name).cloned().or_else(|| {
- find_html_attribute_value(html, tag.source_start, tag.source_end, attribute_name)
- })
- };
-
- vec![
- (
- "tag_name".to_string(),
- html_zval_string(&tag.name.to_ascii_uppercase()),
- ),
- ("is_tag_closer".to_string(), html_zval_bool(tag.closing)),
- (
- "attribute_value".to_string(),
- attribute_value
- .as_deref()
- .map(html_zval_string)
- .unwrap_or_else(html_zval_null),
- ),
- ]
-}
-
-#[cfg(feature = "php-extension")]
-fn html_matching_tag_attributes_public_summary_row(
- html: &[u8],
- tag: &HtmlTag,
- attribute_names: &[String],
- attribute_initial_bits: u32,
-) -> Vec<(String, Zval)> {
- let attribute_values = if tag.closing
- || attribute_names.is_empty()
- || (attribute_initial_bits != 0
- && 0 == (tag.attribute_name_initials & attribute_initial_bits))
- {
- Vec::new()
- } else {
- find_html_attribute_values(html, tag.source_start, tag.source_end, attribute_names)
- };
-
- let mut values = Vec::with_capacity(attribute_names.len());
- for (index, attribute_name) in attribute_names.iter().enumerate() {
- let value = attribute_values.get(index).and_then(|value| value.as_ref());
- values.push((
- attribute_name.as_str(),
- value
- .map(|value| html_zval_string(value))
- .unwrap_or_else(html_zval_null),
- ));
- }
-
- vec![
- (
- "tag_name".to_string(),
- html_zval_string(&tag.name.to_ascii_uppercase()),
- ),
- ("is_tag_closer".to_string(), html_zval_bool(tag.closing)),
- (
- "attribute_values".to_string(),
- html_zval_array(values.into_iter().collect()),
- ),
- ]
-}
-
-#[cfg(feature = "php-extension")]
-fn html_token_public_summary_row(tag: &HtmlTag) -> Vec<(String, Zval)> {
- let token_name = if tag.token_type == "#tag" {
- tag.name.to_ascii_uppercase()
- } else if tag.token_type == "#doctype" {
- tag.name.clone()
- } else {
- tag.token_type.clone()
- };
- let breadcrumbs = tag
- .breadcrumbs
- .iter()
- .map(|breadcrumb| html_zval_string(breadcrumb))
- .collect();
-
- vec![
- ("token_type".to_string(), html_zval_string(&tag.token_type)),
- ("token_name".to_string(), html_zval_string(&token_name)),
- ("is_tag_closer".to_string(), html_zval_bool(tag.closing)),
- ("current_depth".to_string(), html_zval_i64(tag.depth as i64)),
- ("breadcrumbs".to_string(), html_zval_array(breadcrumbs)),
- ]
-}
-
-#[cfg(feature = "php-extension")]
-fn html_zval_string(value: &str) -> Zval {
- let mut zval = Zval::new();
- let _ = zval.set_string(value, false);
- zval
-}
-
-#[cfg(feature = "php-extension")]
-fn html_zval_bool(value: bool) -> Zval {
- let mut zval = Zval::new();
- zval.set_bool(value);
- zval
-}
-
-#[cfg(feature = "php-extension")]
-fn html_zval_i64(value: i64) -> Zval {
- let mut zval = Zval::new();
- zval.set_long(value);
- zval
-}
-
-#[cfg(feature = "php-extension")]
-fn html_zval_null() -> Zval {
- Zval::null()
-}
-
-#[cfg(feature = "php-extension")]
-fn html_zval_array(value: ZBox) -> Zval {
- let mut zval = Zval::new();
- zval.set_hashtable(value);
- zval
-}
-
-fn html_escape_text(value: &str) -> String {
- let mut escaped = String::with_capacity(value.len());
- for character in value.chars() {
- match character {
- '&' => escaped.push_str("&"),
- '<' => escaped.push_str("<"),
- '>' => escaped.push_str(">"),
- '"' => escaped.push_str("""),
- '\'' => escaped.push_str("'"),
- _ => escaped.push(character),
- }
- }
-
- escaped
-}
-
-fn is_html_ascii_whitespace(byte: u8) -> bool {
- matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x0c)
-}
-
-fn normalize_html_newlines_in_place(text: &mut String) {
- if !text.contains('\r') {
- return;
- }
-
- *text = text.replace("\r\n", "\n").replace('\r', "\n");
-}
-
-#[cfg(feature = "php-extension")]
-fn html_doctype_info_zval(html: &str, tag: &HtmlTag) -> Option {
- if tag.token_type != "#doctype" {
- return None;
- }
-
- let doctype_html = html.get(tag.source_start..tag.source_end)?;
- let method = Function::try_from_method("WP_HTML_Doctype_Info", "from_doctype_token")?;
- let value = method.try_call(vec![&doctype_html]).ok()?;
-
- if value.is_null() {
- None
- } else {
- Some(value)
- }
-}
-
-fn html_token_compact_summary(tag: &HtmlTag) -> String {
- let token_kind = match tag.token_type.as_str() {
- "#tag" => "t",
- "#comment" => "c",
- "#doctype" => "d",
- "#text" => "s",
- _ => "o",
- };
- let token_name = if tag.token_type == "#tag" {
- Cow::Owned(tag.name.to_ascii_uppercase())
- } else if tag.token_type == "#doctype" {
- Cow::Borrowed(tag.name.as_str())
- } else {
- Cow::Borrowed(tag.token_type.as_str())
- };
-
- let mut summary = String::with_capacity(
- token_name.len() + tag.breadcrumbs.iter().map(String::len).sum::() + 16,
- );
- summary.push_str(token_kind);
- summary.push('\x1f');
- summary.push_str(&token_name);
- summary.push('\x1f');
- summary.push(if tag.closing { '1' } else { '0' });
- summary.push('\x1f');
- summary.push_str(&tag.depth.to_string());
- summary.push('\x1f');
- summary.push_str(&tag.breadcrumbs.join("\x1d"));
-
- summary
-}
-
-#[cfg_attr(feature = "php-extension", allow(dead_code))]
-pub fn parse_html_tags(html: &str) -> Vec {
- let bytes = html.as_bytes();
- let mut tags = Vec::new();
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
-
- while let Some(tag) = parse_next_html_token(
- bytes,
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- false,
- false,
- ) {
- tags.push(tag);
- }
-
- tags
-}
-
-fn initial_html_breadcrumbs() -> Vec {
- vec!["HTML".to_string(), "BODY".to_string()]
-}
-
-fn parse_next_html_token(
- bytes: &[u8],
- offset: &mut usize,
- breadcrumbs: &mut Vec,
- include_text: bool,
- include_attribute_values: bool,
- synthesize_implied_closers: bool,
- ignore_html_body_starts: bool,
-) -> Option {
- if *offset >= bytes.len() {
- if synthesize_implied_closers {
- return synthesize_html_implied_closer_token(*offset, breadcrumbs);
- }
-
- return None;
- }
-
- while *offset < bytes.len() {
- let open = match find_byte(bytes, b'<', *offset) {
- Some(open) => open,
- None => {
- let text_start = *offset;
- let text_end = bytes.len();
- if synthesize_implied_closers {
- if is_html_table_form_context(breadcrumbs) {
- return synthesize_html_implied_closer_token(text_start, breadcrumbs);
- }
-
- let leading_null_end = html_leading_null_end(bytes, text_start, text_end);
- if leading_null_end > text_start {
- *offset = leading_null_end;
- continue;
- }
-
- let leading_whitespace_end =
- html_leading_whitespace_end(bytes, text_start, text_end);
- if leading_whitespace_end > text_start && leading_whitespace_end < text_end {
- let token = html_processor_text_token(
- bytes,
- text_start,
- leading_whitespace_end,
- breadcrumbs,
- );
- *offset = leading_whitespace_end;
- if include_text {
- return token;
- }
-
- continue;
- }
-
- if is_html_table_text_abort_context(breadcrumbs)
- && bytes[text_start..text_end]
- .iter()
- .any(|byte| !is_html_ascii_whitespace(*byte))
- {
- breadcrumbs.truncate(2);
- *offset = bytes.len();
- return None;
- }
- }
-
- let token = if synthesize_implied_closers {
- html_processor_text_token(bytes, text_start, text_end, breadcrumbs)
- } else {
- html_text_token(bytes, text_start, text_end, breadcrumbs)
- };
- *offset = text_end;
- if include_text {
- return token;
- }
-
- if synthesize_implied_closers {
- return synthesize_html_implied_closer_token(*offset, breadcrumbs);
- }
-
- return None;
- }
- };
-
- if open > *offset {
- if synthesize_implied_closers && is_html_table_form_context(breadcrumbs) {
- return synthesize_html_implied_closer_token(*offset, breadcrumbs);
- }
-
- if starts_invalid_html_opening_text(bytes, open) {
- let text_end = find_html_invalid_opening_text_end(bytes, open);
- let token = if synthesize_implied_closers {
- html_processor_text_token(bytes, *offset, text_end, breadcrumbs)
- } else {
- html_text_token(bytes, *offset, text_end, breadcrumbs)
- };
- *offset = text_end;
- if include_text && token.is_some() {
- return token;
- }
- continue;
- }
-
- if synthesize_implied_closers {
- let leading_null_end = html_leading_null_end(bytes, *offset, open);
- if leading_null_end > *offset {
- *offset = leading_null_end;
- continue;
- }
-
- let leading_whitespace_end = html_leading_whitespace_end(bytes, *offset, open);
- if leading_whitespace_end > *offset && leading_whitespace_end < open {
- let token = html_processor_text_token(
- bytes,
- *offset,
- leading_whitespace_end,
- breadcrumbs,
- );
- *offset = leading_whitespace_end;
- if include_text && token.is_some() {
- return token;
- }
-
- continue;
- }
-
- if is_html_table_text_abort_context(breadcrumbs)
- && bytes[*offset..open]
- .iter()
- .any(|byte| !is_html_ascii_whitespace(*byte))
- {
- breadcrumbs.truncate(2);
- *offset = bytes.len();
- return None;
- }
- }
-
- let token = if synthesize_implied_closers {
- html_processor_text_token(bytes, *offset, open, breadcrumbs)
- } else {
- html_text_token(bytes, *offset, open, breadcrumbs)
- };
- *offset = open;
- if include_text && token.is_some() {
- return token;
- }
- }
-
- let mut cursor = open + 1;
- if cursor >= bytes.len() {
- *offset = bytes.len();
- return None;
- }
-
- if bytes[cursor] == b'!'
- && cursor + 2 < bytes.len()
- && bytes[cursor + 1] == b'-'
- && bytes[cursor + 2] == b'-'
- {
- if synthesize_implied_closers && is_html_table_form_context(breadcrumbs) {
- return synthesize_html_implied_closer_token(open, breadcrumbs);
- }
-
- let comment_start = cursor + 3;
- if comment_start < bytes.len()
- && (bytes[comment_start] == b'>'
- || (bytes[comment_start] == b'-'
- && comment_start + 1 < bytes.len()
- && bytes[comment_start + 1] == b'>'))
- {
- let mut token_breadcrumbs = breadcrumbs.clone();
- token_breadcrumbs.push("#comment".to_string());
- *offset = if bytes[comment_start] == b'>' {
- comment_start + 1
- } else {
- comment_start + 2
- };
-
- return Some(HtmlTag {
- name: "#comment".to_string(),
- token_type: "#comment".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: String::new(),
- comment_type: Some("COMMENT_AS_ABRUPTLY_CLOSED_COMMENT".to_string()),
- full_comment_text: Some(String::new()),
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- });
- }
-
- let Some((comment_end, comment_close_len)) =
- find_html_comment_end(bytes, comment_start)
- else {
- *offset = bytes.len();
- return None;
- };
- let mut token_breadcrumbs = breadcrumbs.clone();
- token_breadcrumbs.push("#comment".to_string());
- *offset = comment_end + comment_close_len;
-
- return Some(HtmlTag {
- name: "#comment".to_string(),
- token_type: "#comment".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: String::from_utf8_lossy(&bytes[comment_start..comment_end]).into_owned(),
- comment_type: Some("COMMENT_AS_HTML_COMMENT".to_string()),
- full_comment_text: Some(
- String::from_utf8_lossy(&bytes[comment_start..comment_end]).into_owned(),
- ),
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- });
- }
-
- if bytes[cursor] == b'!' && is_ascii_case_insensitive_prefix(bytes, cursor + 1, b"DOCTYPE")
- {
- let doctype_text_start = cursor + 8;
- let doctype_end = find_byte(bytes, b'>', doctype_text_start).unwrap_or(bytes.len());
- let doctype_text =
- String::from_utf8_lossy(&bytes[doctype_text_start..doctype_end]).into_owned();
- *offset = if doctype_end < bytes.len() {
- doctype_end + 1
- } else {
- bytes.len()
- };
-
- return Some(HtmlTag {
- name: "html".to_string(),
- token_type: "#doctype".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: doctype_text,
- comment_type: None,
- full_comment_text: None,
- depth: 1,
- breadcrumbs: vec!["html".to_string()],
- });
- }
-
- if bytes[cursor] == b'!' && is_ascii_case_insensitive_prefix(bytes, cursor + 1, b"[CDATA[")
- {
- let cdata_text_start = cursor + 8;
- if let Some(cdata_end) = find_html_cdata_end(bytes, cdata_text_start) {
- let mut token_breadcrumbs = breadcrumbs.clone();
- token_breadcrumbs.push("#comment".to_string());
- *offset = cdata_end + 3;
-
- return Some(HtmlTag {
- name: "#comment".to_string(),
- token_type: "#comment".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: String::from_utf8_lossy(&bytes[cdata_text_start..cdata_end]).into_owned(),
- comment_type: Some("COMMENT_AS_CDATA_LOOKALIKE".to_string()),
- full_comment_text: Some(
- String::from_utf8_lossy(&bytes[cursor + 1..cdata_end + 2]).into_owned(),
- ),
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- });
- }
- }
-
- if bytes[cursor] == b'?' {
- let comment_end = find_byte(bytes, b'>', cursor).unwrap_or(bytes.len());
- let full_comment_text =
- String::from_utf8_lossy(&bytes[cursor..comment_end]).into_owned();
- let mut token_breadcrumbs = breadcrumbs.clone();
- token_breadcrumbs.push("#comment".to_string());
- *offset = if comment_end < bytes.len() {
- comment_end + 1
- } else {
- bytes.len()
- };
-
- if comment_end > cursor && bytes[comment_end - 1] == b'?' {
- let target_start = cursor + 1;
- let target_end = span_html_pi_target(bytes, target_start);
- if target_end > target_start {
- return Some(HtmlTag {
- name: String::from_utf8_lossy(&bytes[target_start..target_end])
- .into_owned(),
- token_type: "#comment".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: String::from_utf8_lossy(&bytes[target_end..comment_end - 1])
- .into_owned(),
- comment_type: Some("COMMENT_AS_PI_NODE_LOOKALIKE".to_string()),
- full_comment_text: Some(full_comment_text),
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- });
- }
- }
-
- return Some(HtmlTag {
- name: "#comment".to_string(),
- token_type: "#comment".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: String::from_utf8_lossy(&bytes[cursor + 1..comment_end]).into_owned(),
- comment_type: Some("COMMENT_AS_INVALID_HTML".to_string()),
- full_comment_text: Some(full_comment_text),
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- });
- }
-
- if bytes[cursor] == b'!' {
- let comment_start = cursor + 1;
- let comment_end = find_byte(bytes, b'>', comment_start).unwrap_or(bytes.len());
- let text = String::from_utf8_lossy(&bytes[comment_start..comment_end]).into_owned();
- let mut token_breadcrumbs = breadcrumbs.clone();
- token_breadcrumbs.push("#comment".to_string());
- *offset = if comment_end < bytes.len() {
- comment_end + 1
- } else {
- bytes.len()
- };
-
- return Some(HtmlTag {
- name: "#comment".to_string(),
- token_type: "#comment".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: text.clone(),
- comment_type: Some("COMMENT_AS_INVALID_HTML".to_string()),
- full_comment_text: Some(text),
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- });
- }
-
- let closing = bytes[cursor] == b'/';
- if closing {
- cursor += 1;
- }
-
- if closing && cursor < bytes.len() && bytes[cursor] == b'>' {
- let mut token_breadcrumbs = breadcrumbs.clone();
- token_breadcrumbs.push("#presumptuous-tag".to_string());
- *offset = cursor + 1;
-
- return Some(HtmlTag {
- name: "#presumptuous-tag".to_string(),
- token_type: "#presumptuous-tag".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: String::new(),
- comment_type: None,
- full_comment_text: None,
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- });
- }
-
- if closing && (cursor >= bytes.len() || !bytes[cursor].is_ascii_alphabetic()) {
- let comment_end = find_byte(bytes, b'>', cursor).unwrap_or(bytes.len());
- let text = String::from_utf8_lossy(&bytes[cursor..comment_end]).into_owned();
- let mut token_breadcrumbs = breadcrumbs.clone();
- token_breadcrumbs.push("#funky-comment".to_string());
- *offset = if comment_end < bytes.len() {
- comment_end + 1
- } else {
- bytes.len()
- };
-
- return Some(HtmlTag {
- name: "#funky-comment".to_string(),
- token_type: "#funky-comment".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: open,
- source_end: *offset,
- text: text.clone(),
- comment_type: None,
- full_comment_text: Some(text),
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- });
- }
-
- if !closing && (cursor >= bytes.len() || !bytes[cursor].is_ascii_alphabetic()) {
- let text_end = find_html_invalid_opening_text_end(bytes, open);
- let token = html_text_token(bytes, open, text_end, breadcrumbs);
- *offset = text_end;
-
- if include_text && token.is_some() {
- return token;
- }
- continue;
- }
-
- let name_start = cursor;
- cursor = span_name(bytes, cursor);
- if cursor == name_start {
- *offset = open + 1;
- continue;
- }
-
- let name = ascii_lower(&bytes[name_start..cursor]);
- let mut attributes = HashMap::new();
- let mut attribute_order = Vec::new();
- let mut attribute_name_initials = 0u32;
-
- while cursor < bytes.len() {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= bytes.len() || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- attribute_name_initials |= html_attribute_initial_bit(bytes[attr_start]);
- let attr_name = if include_attribute_values {
- Some(ascii_lower(&bytes[attr_start..cursor]))
- } else {
- None
- };
- cursor = skip_ascii_whitespace(bytes, cursor);
-
- if cursor < bytes.len() && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- if include_attribute_values {
- let (parsed, next) = parse_attribute_value(bytes, cursor);
- cursor = next;
-
- if let Some(name) = attr_name.as_ref() {
- if !attributes.contains_key(name) {
- attributes.insert(name.clone(), parsed);
- }
- }
- } else {
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- } else if let Some(name) = attr_name.as_ref() {
- if !attributes.contains_key(name) {
- attributes.insert(name.clone(), String::new());
- }
- }
-
- if let Some(name) = attr_name {
- if !attribute_order
- .iter()
- .any(|existing_name| existing_name == &name)
- {
- attribute_order.push(name);
- }
- }
- }
-
- let tag_end = find_byte(bytes, b'>', cursor).unwrap_or(bytes.len());
- if tag_end >= bytes.len() {
- *offset = bytes.len();
- return None;
- }
-
- let self_closing = cursor < bytes.len() && bytes[cursor] == b'/';
- let uppercase_name = name.to_ascii_uppercase();
-
- if synthesize_implied_closers
- && !closing
- && !self_closing
- && name != "form"
- && is_html_table_form_context(breadcrumbs)
- {
- return synthesize_html_implied_closer_token(open, breadcrumbs);
- }
-
- if synthesize_implied_closers
- && !closing
- && !self_closing
- && ignore_html_body_starts
- && matches!(name.as_str(), "html" | "body")
- {
- *offset = tag_end.saturating_add(1).min(bytes.len());
- continue;
- }
-
- if synthesize_implied_closers
- && !closing
- && !self_closing
- && name == "form"
- && breadcrumbs.iter().any(|open| open == "FORM")
- {
- *offset = tag_end.saturating_add(1).min(bytes.len());
- continue;
- }
-
- if synthesize_implied_closers
- && !closing
- && !self_closing
- && should_synthesize_html_table_form_closer(&name, breadcrumbs)
- {
- return synthesize_html_implied_closer_token(open, breadcrumbs);
- }
-
- if synthesize_implied_closers
- && !closing
- && !self_closing
- && should_synthesize_html_implied_colgroup(&name, breadcrumbs)
- {
- return Some(synthesize_html_implied_opener_token(
- "colgroup",
- open,
- breadcrumbs,
- ));
- }
-
- if synthesize_implied_closers
- && !closing
- && !self_closing
- && should_synthesize_html_implied_table_body(&name, breadcrumbs)
- {
- return Some(synthesize_html_implied_opener_token(
- "tbody",
- open,
- breadcrumbs,
- ));
- }
-
- if synthesize_implied_closers
- && !closing
- && !self_closing
- && should_synthesize_html_implied_table_row(&name, breadcrumbs)
- {
- return Some(synthesize_html_implied_opener_token(
- "tr",
- open,
- breadcrumbs,
- ));
- }
-
- if synthesize_implied_closers
- && closing
- && should_synthesize_html_implied_closer_before_closing(&uppercase_name, breadcrumbs)
- {
- return synthesize_html_implied_closer_token(open, breadcrumbs);
- }
-
- if synthesize_implied_closers
- && closing
- && uppercase_name == "SELECT"
- && !breadcrumbs.iter().any(|open| open == "SELECT")
- {
- *offset = tag_end.saturating_add(1).min(bytes.len());
- continue;
- }
-
- if synthesize_implied_closers
- && closing
- && should_abort_html_table_child_closer(&name, breadcrumbs)
- {
- breadcrumbs.truncate(2);
- *offset = bytes.len();
- return None;
- }
-
- if synthesize_implied_closers
- && closing
- && !html_breadcrumb_contains_open_element(breadcrumbs, &uppercase_name)
- {
- *offset = tag_end.saturating_add(1).min(bytes.len());
- continue;
- }
-
- if synthesize_implied_closers
- && !closing
- && !self_closing
- && should_synthesize_html_implied_closer(&name, breadcrumbs)
- {
- return synthesize_html_implied_closer_token(open, breadcrumbs);
- }
-
- if synthesize_implied_closers
- && !closing
- && should_abort_html_table_child_start(&name, breadcrumbs)
- {
- breadcrumbs.truncate(2);
- *offset = bytes.len();
- return None;
- }
-
- let stop_after_table_form_closer = synthesize_implied_closers
- && closing
- && uppercase_name == "FORM"
- && breadcrumbs.len() >= 4
- && breadcrumbs.last().map(|last| last.as_str()) == Some("FORM")
- && breadcrumbs
- .get(breadcrumbs.len().saturating_sub(2))
- .map(|ancestor| ancestor.as_str())
- == Some("TABLE");
-
- let token_breadcrumbs = if closing {
- pop_html_breadcrumb(breadcrumbs, &uppercase_name);
- breadcrumbs.clone()
- } else {
- let mut next_breadcrumbs = breadcrumbs.clone();
- next_breadcrumbs.push(uppercase_name.clone());
- if !is_html_void_element(&name) {
- breadcrumbs.push(uppercase_name.clone());
- }
- next_breadcrumbs
- };
- let depth = token_breadcrumbs.len();
- let mut text = String::new();
-
- *offset = if !closing
- && !self_closing
- && is_html_raw_text_element(&name)
- && tag_end < bytes.len()
- {
- let content_start = tag_end + 1;
- match find_html_closing_tag(bytes, &name, content_start) {
- Some(closing_start) => {
- text = parse_html_raw_text_content(&name, &bytes[content_start..closing_start]);
- pop_html_breadcrumb(breadcrumbs, &uppercase_name);
- find_byte(bytes, b'>', closing_start).map_or(bytes.len(), |close| close + 1)
- }
- None => {
- text = parse_html_raw_text_content(&name, &bytes[content_start..]);
- pop_html_breadcrumb(breadcrumbs, &uppercase_name);
- bytes.len()
- }
- }
- } else {
- tag_end.saturating_add(1).min(bytes.len())
- };
-
- if stop_after_table_form_closer {
- breadcrumbs.truncate(2);
- *offset = bytes.len();
- }
-
- return Some(HtmlTag {
- name,
- token_type: "#tag".to_string(),
- closing,
- attributes,
- attribute_order,
- attribute_name_initials,
- source_start: open,
- source_end: tag_end.saturating_add(1).min(bytes.len()),
- text,
- comment_type: None,
- full_comment_text: None,
- breadcrumbs: token_breadcrumbs,
- depth,
- });
- }
-
- None
-}
-
-fn parse_next_plain_html_tag_token(bytes: &[u8], offset: &mut usize) -> Option {
- while *offset < bytes.len() {
- let open = match find_byte(bytes, b'<', *offset) {
- Some(open) => open,
- None => {
- *offset = bytes.len();
- return None;
- }
- };
-
- if open + 1 >= bytes.len() {
- *offset = bytes.len();
- return None;
- }
-
- if open > *offset && starts_invalid_html_opening_text(bytes, open) {
- *offset = find_html_invalid_opening_text_end(bytes, open);
- continue;
- }
-
- let mut cursor = open + 1;
-
- if bytes[cursor] == b'!'
- && cursor + 2 < bytes.len()
- && bytes[cursor + 1] == b'-'
- && bytes[cursor + 2] == b'-'
- {
- let comment_start = cursor + 3;
- if comment_start < bytes.len()
- && (bytes[comment_start] == b'>'
- || (bytes[comment_start] == b'-'
- && comment_start + 1 < bytes.len()
- && bytes[comment_start + 1] == b'>'))
- {
- *offset = if bytes[comment_start] == b'>' {
- comment_start + 1
- } else {
- comment_start + 2
- };
- continue;
- }
-
- let Some((comment_end, comment_close_len)) =
- find_html_comment_end(bytes, comment_start)
- else {
- *offset = bytes.len();
- return None;
- };
-
- *offset = comment_end + comment_close_len;
- continue;
- }
-
- if bytes[cursor] == b'!' && is_ascii_case_insensitive_prefix(bytes, cursor + 1, b"DOCTYPE")
- {
- let doctype_text_start = cursor + 8;
- let doctype_end = find_byte(bytes, b'>', doctype_text_start).unwrap_or(bytes.len());
- *offset = if doctype_end < bytes.len() {
- doctype_end + 1
- } else {
- bytes.len()
- };
- continue;
- }
-
- if bytes[cursor] == b'!' && is_ascii_case_insensitive_prefix(bytes, cursor + 1, b"[CDATA[")
- {
- let cdata_text_start = cursor + 8;
- *offset = find_html_cdata_end(bytes, cdata_text_start)
- .and_then(|cdata_end| cdata_end.checked_add(3))
- .or_else(|| find_byte(bytes, b'>', cdata_text_start).map(|end| end + 1))
- .unwrap_or(bytes.len());
- continue;
- }
-
- if bytes[cursor] == b'?' {
- *offset = find_byte(bytes, b'>', cursor).map_or(bytes.len(), |end| end + 1);
- continue;
- }
-
- if bytes[cursor] == b'!' {
- *offset = find_byte(bytes, b'>', cursor + 1).map_or(bytes.len(), |end| end + 1);
- continue;
- }
-
- let closing = bytes[cursor] == b'/';
- if closing {
- cursor += 1;
- }
-
- if closing && cursor < bytes.len() && bytes[cursor] == b'>' {
- *offset = cursor + 1;
- continue;
- }
-
- if closing && (cursor >= bytes.len() || !bytes[cursor].is_ascii_alphabetic()) {
- *offset = find_byte(bytes, b'>', cursor).map_or(bytes.len(), |end| end + 1);
- continue;
- }
-
- if !closing && (cursor >= bytes.len() || !bytes[cursor].is_ascii_alphabetic()) {
- *offset = find_html_invalid_opening_text_end(bytes, open);
- continue;
- }
-
- let name_start = cursor;
- cursor = span_name(bytes, cursor);
- if cursor == name_start {
- *offset = open + 1;
- continue;
- }
-
- let name = ascii_lower(&bytes[name_start..cursor]);
- let mut attribute_name_initials = 0u32;
-
- while cursor < bytes.len() {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= bytes.len() || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- attribute_name_initials |= html_attribute_initial_bit(bytes[attr_start]);
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor < bytes.len() && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- }
-
- let tag_end = find_byte(bytes, b'>', cursor).unwrap_or(bytes.len());
- if tag_end >= bytes.len() {
- *offset = bytes.len();
- return None;
- }
-
- let self_closing = cursor < bytes.len() && bytes[cursor] == b'/';
- let mut text = String::new();
-
- *offset = if !closing && !self_closing && is_html_raw_text_element(&name) {
- let content_start = tag_end + 1;
- match find_html_closing_tag(bytes, &name, content_start) {
- Some(closing_start) => {
- text = parse_html_raw_text_content(&name, &bytes[content_start..closing_start]);
- find_byte(bytes, b'>', closing_start).map_or(bytes.len(), |close| close + 1)
- }
- None => {
- text = parse_html_raw_text_content(&name, &bytes[content_start..]);
- bytes.len()
- }
- }
- } else {
- tag_end + 1
- };
-
- return Some(HtmlTag {
- name,
- token_type: "#tag".to_string(),
- closing,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials,
- source_start: open,
- source_end: tag_end + 1,
- text,
- comment_type: None,
- full_comment_text: None,
- breadcrumbs: Vec::new(),
- depth: 0,
- });
- }
-
- None
-}
-
-fn html_text_token(
- bytes: &[u8],
- start: usize,
- end: usize,
- breadcrumbs: &[String],
-) -> Option {
- if end <= start {
- return None;
- }
-
- let text = decode_html_text_value(&String::from_utf8_lossy(&bytes[start..end]));
- if text.is_empty() {
- return None;
- }
-
- let mut token_breadcrumbs = breadcrumbs.to_vec();
- token_breadcrumbs.push("#text".to_string());
- Some(HtmlTag {
- name: "#text".to_string(),
- token_type: "#text".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: start,
- source_end: end,
- text,
- comment_type: None,
- full_comment_text: None,
- depth: token_breadcrumbs.len(),
- breadcrumbs: token_breadcrumbs,
- })
-}
-
-fn html_processor_text_token(
- bytes: &[u8],
- start: usize,
- end: usize,
- breadcrumbs: &[String],
-) -> Option {
- let mut token = html_text_token(bytes, start, end, breadcrumbs)?;
- token.text.retain(|character| character != '\0');
- normalize_html_newlines_in_place(&mut token.text);
- if token.text.is_empty() {
- return None;
- }
-
- Some(token)
-}
-
-fn html_leading_null_end(bytes: &[u8], start: usize, end: usize) -> usize {
- let mut cursor = start;
- while cursor < end && bytes[cursor] == b'\0' {
- cursor += 1;
- }
-
- cursor
-}
-
-fn html_leading_whitespace_end(bytes: &[u8], start: usize, end: usize) -> usize {
- let mut cursor = start;
- while cursor < end {
- if is_html_ascii_whitespace(bytes[cursor]) {
- cursor += 1;
- continue;
- }
-
- if bytes[cursor] == b'&' {
- if let Some((replacement, next)) =
- decode_html_character_reference(bytes, cursor + 1, HtmlDecodeContext::Text)
- {
- let replacement = replacement.as_ref().as_bytes();
- if replacement.len() == 1 && is_html_ascii_whitespace(replacement[0]) {
- cursor = next.min(end);
- continue;
- }
- }
- }
-
- break;
- }
-
- cursor
-}
-
-fn is_html_table_text_abort_context(breadcrumbs: &[String]) -> bool {
- matches!(
- breadcrumbs.last().map(|name| name.as_str()),
- Some("TABLE" | "TBODY" | "THEAD" | "TFOOT" | "TR" | "COLGROUP")
- )
-}
-
-fn parse_attribute_value(bytes: &[u8], cursor: usize) -> (String, usize) {
- if cursor >= bytes.len() {
- return (String::new(), cursor);
- }
-
- if bytes[cursor] == b'"' || bytes[cursor] == b'\'' {
- let quote = bytes[cursor];
- let start = cursor + 1;
- let end = find_byte(bytes, quote, start).unwrap_or(bytes.len());
- return (
- decode_html_attribute_value(&String::from_utf8_lossy(&bytes[start..end])),
- end + 1,
- );
- }
-
- let end = span_unquoted_value(bytes, cursor);
- (
- decode_html_attribute_value(&String::from_utf8_lossy(&bytes[cursor..end])),
- end,
- )
-}
-
-fn skip_html_attribute_value(bytes: &[u8], cursor: usize) -> usize {
- if cursor >= bytes.len() {
- return cursor;
- }
-
- if bytes[cursor] == b'"' || bytes[cursor] == b'\'' {
- let quote = bytes[cursor];
- let end = find_byte(bytes, quote, cursor + 1).unwrap_or(bytes.len());
- return end.saturating_add(1).min(bytes.len());
- }
-
- span_unquoted_value(bytes, cursor)
-}
-
-fn html_tag_has_self_closing_flag(bytes: &[u8], source_start: usize, source_end: usize) -> bool {
- if source_start >= bytes.len() || source_end <= source_start {
- return false;
- }
-
- let mut cursor = source_end.min(bytes.len());
- while cursor > source_start && bytes[cursor - 1].is_ascii_whitespace() {
- cursor -= 1;
- }
- if cursor <= source_start || bytes[cursor - 1] != b'>' {
- return false;
- }
-
- cursor -= 1;
- while cursor > source_start && bytes[cursor - 1].is_ascii_whitespace() {
- cursor -= 1;
- }
-
- cursor > source_start && bytes[cursor - 1] == b'/'
-}
-
-fn find_html_attribute_value(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_name: &str,
-) -> Option {
- if source_start >= bytes.len() || source_end <= source_start {
- return None;
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return None;
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- let attr_name = ascii_lower(&bytes[attr_start..cursor]);
- cursor = skip_ascii_whitespace(bytes, cursor);
-
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- if attr_name == wanted_name {
- return Some(parse_attribute_value(bytes, cursor).0);
- }
-
- cursor = skip_html_attribute_value(bytes, cursor);
- } else if attr_name == wanted_name {
- return Some(String::new());
- }
- }
-
- None
-}
-
-fn find_html_attribute_has_value(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_name: &str,
-) -> Option {
- if source_start >= bytes.len() || source_end <= source_start {
- return None;
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return None;
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- let attr_name = ascii_lower(&bytes[attr_start..cursor]);
- cursor = skip_ascii_whitespace(bytes, cursor);
-
- if attr_name == wanted_name {
- return Some(cursor < tag_end && bytes[cursor] == b'=');
- }
-
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- }
-
- None
-}
-
-fn find_html_attribute_span(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_name: &str,
-) -> Option<(usize, usize)> {
- if source_start >= bytes.len() || source_end <= source_start {
- return None;
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return None;
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- let attr_name = ascii_lower(&bytes[attr_start..cursor]);
- let attr_name_end = cursor;
- cursor = skip_ascii_whitespace(bytes, cursor);
-
- let mut attr_end = attr_name_end;
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- attr_end = cursor;
- }
-
- if attr_name == wanted_name {
- return Some((attr_start, attr_end.saturating_sub(attr_start)));
- }
- }
-
- None
-}
-
-fn html_tag_name_end(bytes: &[u8], source_start: usize, source_end: usize) -> usize {
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- cursor += 1;
- }
-
- span_name(bytes, skip_ascii_whitespace(bytes, cursor))
-}
-
-fn is_valid_html_attribute_name(name: &str) -> bool {
- if name.is_empty() {
- return false;
- }
-
- !name.chars().any(|ch| {
- matches!(
- ch,
- '"' | '\'' | '>' | '&' | '<' | '/' | '=' | ' ' | '\t' | '\n' | '\r' | '\u{0c}'
- ) || ch.is_control()
- || matches!(
- ch as u32,
- 0xfdd0
- ..=0xfdef
- | 0xfffe
- | 0xffff
- | 0x1fffe
- | 0x1ffff
- | 0x2fffe
- | 0x2ffff
- | 0x3fffe
- | 0x3ffff
- | 0x4fffe
- | 0x4ffff
- | 0x5fffe
- | 0x5ffff
- | 0x6fffe
- | 0x6ffff
- | 0x7fffe
- | 0x7ffff
- | 0x8fffe
- | 0x8ffff
- | 0x9fffe
- | 0x9ffff
- | 0xafffe
- | 0xaffff
- | 0xbfffe
- | 0xbffff
- | 0xcfffe
- | 0xcffff
- | 0xdfffe
- | 0xdffff
- | 0xefffe
- | 0xeffff
- | 0xffffe
- | 0xfffff
- | 0x10fffe
- | 0x10ffff
- )
- })
-}
-
-fn html_escape_attribute_value(value: &str) -> String {
- let mut escaped = String::with_capacity(value.len());
- for ch in value.chars() {
- match ch {
- '&' => escaped.push_str("&"),
- '"' => escaped.push_str("""),
- '<' => escaped.push_str("<"),
- '>' => escaped.push_str(">"),
- _ => escaped.push(ch),
- }
- }
- escaped
-}
-
-fn find_html_attribute_values(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_names: &[String],
-) -> Vec> {
- let mut values = vec![None; wanted_names.len()];
- if wanted_names.is_empty() || source_start >= bytes.len() || source_end <= source_start {
- return values;
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return values;
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- while cursor < tag_end && values.iter().any(|value| value.is_none()) {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- let attr_name = &bytes[attr_start..cursor];
- cursor = skip_ascii_whitespace(bytes, cursor);
-
- let matching_index = wanted_names
- .iter()
- .enumerate()
- .find_map(|(index, wanted_name)| {
- if values[index].is_none() && attr_name.eq_ignore_ascii_case(wanted_name.as_bytes())
- {
- Some(index)
- } else {
- None
- }
- });
-
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- if let Some(index) = matching_index {
- let (parsed, next) = parse_attribute_value(bytes, cursor);
- values[index] = Some(parsed);
- cursor = next;
- } else {
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- } else if let Some(index) = matching_index {
- values[index] = Some(String::new());
- }
- }
-
- values
-}
-
-fn find_html_attribute_names_with_prefix(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_prefix: &str,
-) -> Option> {
- if source_start >= bytes.len() || source_end <= source_start {
- return None;
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return None;
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- let mut matches = Vec::new();
- let mut matched_slices: Vec<&[u8]> = Vec::new();
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- if !ascii_starts_with_ignore_case(&bytes[attr_start..cursor], wanted_prefix.as_bytes()) {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- continue;
- }
-
- let attr_name = &bytes[attr_start..cursor];
- if !matched_slices
- .iter()
- .any(|name| name.eq_ignore_ascii_case(attr_name))
- {
- matched_slices.push(attr_name);
- matches.push(ascii_lower(attr_name));
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- }
-
- Some(matches)
-}
-
-fn find_html_attribute_names_with_prefix_string(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_prefix: &str,
-) -> Option {
- if source_start >= bytes.len() || source_end <= source_start {
- return None;
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return None;
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- let mut matches = String::new();
- let mut matched_slices: Vec<&[u8]> = Vec::new();
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- if !ascii_starts_with_ignore_case(&bytes[attr_start..cursor], wanted_prefix.as_bytes()) {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- continue;
- }
-
- let attr_name = &bytes[attr_start..cursor];
- if !matched_slices
- .iter()
- .any(|name| name.eq_ignore_ascii_case(attr_name))
- {
- if !matches.is_empty() {
- matches.push('\x1f');
- }
-
- matched_slices.push(attr_name);
- push_ascii_lower(&mut matches, attr_name);
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- }
-
- Some(matches)
-}
-
-fn find_html_attribute_names_with_prefix_count(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_prefix: &str,
-) -> Option {
- if source_start >= bytes.len() || source_end <= source_start {
- return None;
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return None;
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- let mut count = 0;
- let mut matches: Vec<&[u8]> = Vec::new();
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- if ascii_starts_with_ignore_case(&bytes[attr_start..cursor], wanted_prefix.as_bytes()) {
- let attr_name = &bytes[attr_start..cursor];
- if !matches
- .iter()
- .any(|name| name.eq_ignore_ascii_case(attr_name))
- {
- matches.push(attr_name);
- count += 1;
- }
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- }
- }
-
- Some(count)
-}
-
-fn find_html_attribute_removals(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_name: &str,
-) -> Vec<(usize, usize)> {
- if source_start >= bytes.len() || source_end <= source_start {
- return Vec::new();
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return Vec::new();
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- let mut removals = Vec::new();
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- let attr_name = ascii_lower(&bytes[attr_start..cursor]);
- let attr_name_end = cursor;
- cursor = skip_ascii_whitespace(bytes, cursor);
-
- let mut attr_end = attr_name_end;
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- attr_end = cursor;
- }
-
- if attr_name == wanted_name {
- removals.push((attr_start, attr_end.saturating_sub(attr_start)));
- }
- }
-
- removals
-}
-
-fn find_html_attribute_removals_with_prefix(
- bytes: &[u8],
- source_start: usize,
- source_end: usize,
- wanted_prefix: &str,
- removed_names: &[String],
-) -> Option {
- if source_start >= bytes.len() || source_end <= source_start {
- return None;
- }
-
- let tag_end = source_end.saturating_sub(1).min(bytes.len());
- let mut cursor = source_start.saturating_add(1);
- if cursor < tag_end && bytes[cursor] == b'/' {
- return None;
- }
-
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = span_name(bytes, cursor);
-
- let mut count = 0;
- let mut names = Vec::new();
- let mut removals = Vec::new();
- while cursor < tag_end {
- cursor = skip_ascii_whitespace(bytes, cursor);
- if cursor >= tag_end || bytes[cursor] == b'>' {
- break;
- }
- if bytes[cursor] == b'/' && cursor + 1 < bytes.len() && bytes[cursor + 1] == b'>' {
- break;
- }
-
- let attr_start = cursor;
- cursor = span_html_attribute_name(bytes, cursor);
- if cursor == attr_start {
- cursor += 1;
- continue;
- }
-
- let attr_name = ascii_lower(&bytes[attr_start..cursor]);
- let attr_name_end = cursor;
- cursor = skip_ascii_whitespace(bytes, cursor);
-
- let mut attr_end = attr_name_end;
- if cursor < tag_end && bytes[cursor] == b'=' {
- cursor += 1;
- cursor = skip_ascii_whitespace(bytes, cursor);
- cursor = skip_html_attribute_value(bytes, cursor);
- attr_end = cursor;
- }
-
- if !attr_name.starts_with(wanted_prefix)
- || removed_names.iter().any(|name| name == &attr_name)
- {
- continue;
- }
-
- removals.push((attr_start, attr_end.saturating_sub(attr_start)));
- if !names.iter().any(|name| name == &attr_name) {
- names.push(attr_name);
- count += 1;
- }
- }
-
- Some((count, removals, names))
-}
-
-fn apply_html_text_removals(html: &str, removals: &[HtmlTextRemoval]) -> String {
- let mut removals: Vec<(usize, usize, usize, &str)> = removals
- .iter()
- .enumerate()
- .map(|(index, removal)| {
- (
- removal.start,
- removal.length,
- index,
- removal.replacement.as_str(),
- )
- })
- .collect();
- removals.sort_unstable_by(|left, right| {
- left.0
- .cmp(&right.0)
- .then_with(|| left.1.cmp(&right.1))
- .then_with(|| {
- if left.1 == 0 && right.1 == 0 {
- right.2.cmp(&left.2)
- } else {
- left.2.cmp(&right.2)
- }
- })
- });
-
- let mut compact: Vec<(usize, usize, &str)> = Vec::with_capacity(removals.len());
- for (start, length, _, replacement) in removals {
- if let Some(last) = compact.last_mut() {
- if last.0 == start && last.1 == length && length != 0 {
- last.2 = replacement;
- continue;
- }
- }
-
- compact.push((start, length, replacement));
- }
-
- let mut updated = String::with_capacity(html.len());
- let mut cursor = 0usize;
-
- for (start, length, replacement) in compact {
- if start < cursor || start > html.len() {
- continue;
- }
-
- updated.push_str(&html[cursor..start]);
- updated.push_str(replacement);
- cursor = start.saturating_add(length).min(html.len());
- }
-
- updated.push_str(&html[cursor..]);
- updated
-}
-
-fn html_attribute_insertion_matches_name(replacement: &str, comparable_name: &str) -> bool {
- let trimmed = replacement.trim_start();
- if trimmed.is_empty() {
- return false;
- }
-
- let name_end = trimmed
- .find(|byte: char| byte == '=' || byte.is_ascii_whitespace())
- .unwrap_or(trimmed.len());
-
- trimmed[..name_end].eq_ignore_ascii_case(comparable_name)
-}
-
-fn ascii_starts_with_ignore_case(bytes: &[u8], prefix: &[u8]) -> bool {
- bytes
- .get(..prefix.len())
- .map(|candidate| candidate.eq_ignore_ascii_case(prefix))
- .unwrap_or(false)
-}
-
-#[derive(Clone, Copy)]
-enum HtmlDecodeContext {
- Attribute,
- Text,
-}
-
-fn decode_html_attribute_value(value: &str) -> String {
- decode_html_value(value, HtmlDecodeContext::Attribute)
-}
-
-fn decode_html_text_value(value: &str) -> String {
- decode_html_value(value, HtmlDecodeContext::Text)
-}
-
-fn decode_html_value(value: &str, context: HtmlDecodeContext) -> String {
- let bytes = value.as_bytes();
- let mut decoded = String::new();
- let mut cursor = 0;
- let mut literal_start = 0;
-
- while cursor < bytes.len() {
- if bytes[cursor] != b'&' {
- cursor += 1;
- continue;
- }
-
- match decode_html_character_reference(bytes, cursor + 1, context) {
- Some((replacement, next)) => {
- decoded.push_str(&String::from_utf8_lossy(&bytes[literal_start..cursor]));
- decoded.push_str(&replacement);
- cursor = next;
- literal_start = next;
- }
- None => {
- cursor += 1;
- }
- }
- }
-
- decoded.push_str(&String::from_utf8_lossy(&bytes[literal_start..]));
-
- decoded
-}
-
-fn parse_html_raw_text_content(name: &str, bytes: &[u8]) -> String {
- let text = String::from_utf8_lossy(bytes);
- if matches!(name, "textarea" | "title") {
- decode_html_text_value(&text)
- } else {
- text.into_owned()
- }
-}
-
-fn decode_html_character_reference(
- bytes: &[u8],
- start: usize,
- context: HtmlDecodeContext,
-) -> Option<(Cow<'static, str>, usize)> {
- if start >= bytes.len() {
- return None;
- }
-
- if bytes[start] == b'#' {
- return decode_numeric_character_reference(bytes, start + 1);
- }
-
- if let Some(name_end) = find_byte(bytes, b';', start) {
- let name = &bytes[start..name_end];
- if let Some(replacement) = decode_named_character_reference(name, true) {
- return Some((Cow::Borrowed(replacement), name_end + 1));
- }
- }
-
- let mut cursor = start;
- while cursor < bytes.len() && bytes[cursor].is_ascii_alphanumeric() {
- cursor += 1;
- }
-
- let name = &bytes[start..cursor];
- let mut match_end = cursor;
- while match_end > start {
- if let Some(replacement) = decode_named_character_reference(&bytes[start..match_end], false)
- {
- let ambiguous_follower = match_end < bytes.len()
- && (bytes[match_end].is_ascii_alphanumeric() || bytes[match_end] == b'=');
-
- if ambiguous_follower && matches!(context, HtmlDecodeContext::Attribute) {
- return None;
- }
-
- return Some((Cow::Borrowed(replacement), match_end));
- }
-
- match_end -= 1;
- }
-
- if name.is_empty() {
- return None;
- }
-
- None
-}
-
-fn decode_named_character_reference(name: &[u8], has_semicolon: bool) -> Option<&'static str> {
- let replacement = match name {
- b"amp" => "&",
- b"lt" => "<",
- b"gt" => ">",
- b"quot" => "\"",
- b"apos" if has_semicolon => "'",
- b"nbsp" => "\u{00a0}",
- b"copy" => "\u{00a9}",
- b"reg" => "\u{00ae}",
- b"not" => "\u{00ac}",
- b"ndash" if has_semicolon => "\u{2013}",
- b"mdash" if has_semicolon => "\u{2014}",
- b"hellip" if has_semicolon => "\u{2026}",
- b"notin" if has_semicolon => "\u{2209}",
- _ => return None,
- };
-
- Some(replacement)
-}
-
-fn decode_numeric_character_reference(
- bytes: &[u8],
- start: usize,
-) -> Option<(Cow<'static, str>, usize)> {
- if start >= bytes.len() {
- return None;
- }
-
- let (radix, digits_start, max_digits) = if bytes[start] == b'x' || bytes[start] == b'X' {
- (16, start + 1, 6)
- } else {
- (10, start, 7)
- };
- let valid_digit = |byte: u8| {
- if radix == 16 {
- byte.is_ascii_hexdigit()
- } else {
- byte.is_ascii_digit()
- }
- };
- let mut digits_end = digits_start;
- while digits_end < bytes.len() && valid_digit(bytes[digits_end]) {
- digits_end += 1;
- }
-
- if digits_end == digits_start {
- return None;
- }
-
- let mut significant_start = digits_start;
- while significant_start < digits_end && bytes[significant_start] == b'0' {
- significant_start += 1;
- }
-
- let semicolon_end = if digits_end < bytes.len() && bytes[digits_end] == b';' {
- digits_end + 1
- } else {
- digits_end
- };
-
- if significant_start == digits_end {
- return Some((Cow::Borrowed("\u{fffd}"), semicolon_end));
- }
-
- if digits_end - significant_start > max_digits {
- return Some((Cow::Borrowed("\u{fffd}"), semicolon_end));
- }
-
- let digits = std::str::from_utf8(&bytes[significant_start..digits_end]).ok()?;
- let mut value = u32::from_str_radix(digits, radix).ok()?;
-
- if (0x80..=0x9f).contains(&value) {
- value = WINDOWS_1252_REPLACEMENTS[(value - 0x80) as usize];
- }
-
- let character = char::from_u32(value).unwrap_or('\u{fffd}');
-
- Some((Cow::Owned(character.to_string()), semicolon_end))
-}
-
-const WINDOWS_1252_REPLACEMENTS: [u32; 32] = [
- 0x20ac, 0x81, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, 0x02c6, 0x2030, 0x0160, 0x2039,
- 0x0152, 0x8d, 0x017d, 0x8f, 0x90, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014,
- 0x02dc, 0x2122, 0x0161, 0x203a, 0x0153, 0x9d, 0x017e, 0x0178,
-];
-
-fn find_byte(bytes: &[u8], needle: u8, start: usize) -> Option {
- bytes
- .get(start..)
- .and_then(|tail| tail.iter().position(|byte| *byte == needle))
- .map(|position| position + start)
-}
-
-fn find_html_comment_end(bytes: &[u8], start: usize) -> Option<(usize, usize)> {
- let tail = bytes.get(start..)?;
- let mut cursor = 0;
- while cursor + 2 < tail.len() {
- if &tail[cursor..cursor + 3] == b"-->" {
- return Some((start + cursor, 3));
- }
- if cursor + 3 < tail.len() && &tail[cursor..cursor + 4] == b"--!>" {
- return Some((start + cursor, 4));
- }
- cursor += 1;
- }
-
- None
-}
-
-fn find_html_cdata_end(bytes: &[u8], start: usize) -> Option {
- bytes.get(start..).and_then(|tail| {
- tail.windows(3)
- .position(|window| window == b"]]>")
- .map(|position| position + start)
- })
-}
-
-fn find_html_invalid_opening_text_end(bytes: &[u8], start: usize) -> usize {
- let mut cursor = start;
-
- while let Some(open) = find_byte(bytes, b'<', cursor) {
- if open > cursor && starts_valid_html_token(bytes, open) {
- return open;
- }
-
- if starts_valid_html_token(bytes, open) {
- return if open == start { start + 1 } else { open };
- }
-
- let invalid_close = find_byte(bytes, b'>', open + 1).unwrap_or(bytes.len());
- if let Some(next_open) = find_byte(bytes, b'<', open + 1) {
- if next_open < invalid_close {
- if starts_valid_html_token(bytes, next_open) {
- return next_open;
- }
-
- cursor = next_open;
- continue;
- }
- }
-
- cursor = invalid_close.saturating_add(1).min(bytes.len());
- }
-
- bytes.len()
-}
-
-fn starts_invalid_html_opening_text(bytes: &[u8], open: usize) -> bool {
- match bytes.get(open + 1) {
- None => true,
- Some(b'!' | b'?' | b'/') => false,
- Some(next) => !next.is_ascii_alphabetic(),
- }
-}
-
-fn starts_valid_html_token(bytes: &[u8], open: usize) -> bool {
- let Some(next) = bytes.get(open + 1) else {
- return false;
- };
-
- matches!(*next, b'!' | b'?' | b'/') || next.is_ascii_alphabetic()
-}
-
-fn is_ascii_case_insensitive_prefix(bytes: &[u8], start: usize, prefix: &[u8]) -> bool {
- bytes
- .get(start..start + prefix.len())
- .map(|candidate| candidate.eq_ignore_ascii_case(prefix))
- .unwrap_or(false)
-}
-
-fn skip_ascii_whitespace(bytes: &[u8], mut cursor: usize) -> usize {
- while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
- cursor += 1;
- }
- cursor
-}
-
-fn span_name(bytes: &[u8], mut cursor: usize) -> usize {
- while cursor < bytes.len()
- && (bytes[cursor].is_ascii_alphanumeric() || matches!(bytes[cursor], b':' | b'_' | b'-'))
- {
- cursor += 1;
- }
- cursor
-}
-
-fn span_html_attribute_name(bytes: &[u8], mut cursor: usize) -> usize {
- if cursor < bytes.len() && bytes[cursor] == b'=' {
- cursor += 1;
- while cursor < bytes.len()
- && !bytes[cursor].is_ascii_whitespace()
- && !matches!(bytes[cursor], b'=' | b'>' | b'/')
- {
- cursor += 1;
- }
- return cursor;
- }
-
- while cursor < bytes.len()
- && !bytes[cursor].is_ascii_whitespace()
- && !matches!(bytes[cursor], b'=' | b'>' | b'/')
- {
- cursor += 1;
- }
- cursor
-}
-
-fn span_html_pi_target(bytes: &[u8], cursor: usize) -> usize {
- if cursor >= bytes.len()
- || !(bytes[cursor].is_ascii_alphabetic() || matches!(bytes[cursor], b':' | b'_'))
- {
- return cursor;
- }
-
- let mut cursor = cursor + 1;
- while cursor < bytes.len()
- && (bytes[cursor].is_ascii_alphanumeric()
- || matches!(bytes[cursor], b':' | b'_' | b'-' | b'.'))
- {
- cursor += 1;
- }
-
- cursor
-}
-
-fn span_unquoted_value(bytes: &[u8], mut cursor: usize) -> usize {
- while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() && bytes[cursor] != b'>' {
- cursor += 1;
- }
- cursor
-}
-
-fn ascii_lower(bytes: &[u8]) -> String {
- bytes
- .iter()
- .map(|byte| byte.to_ascii_lowercase() as char)
- .collect()
-}
-
-fn push_ascii_lower(output: &mut String, bytes: &[u8]) {
- for byte in bytes {
- output.push(byte.to_ascii_lowercase() as char);
- }
-}
-
-fn html_attribute_initial_bit(byte: u8) -> u32 {
- let lower = byte.to_ascii_lowercase();
- if lower.is_ascii_lowercase() {
- 1u32 << (lower - b'a')
- } else {
- 0
- }
-}
-
-fn html_attribute_prefix_initial_bit(prefix: &str) -> u32 {
- prefix
- .as_bytes()
- .first()
- .map(|byte| html_attribute_initial_bit(*byte))
- .unwrap_or(0)
-}
-
-fn html_attribute_names_initial_bits(names: &[String]) -> u32 {
- names.iter().fold(0u32, |bits, name| {
- bits | html_attribute_prefix_initial_bit(name)
- })
-}
-
-fn is_html_void_element(name: &str) -> bool {
- matches!(
- name,
- "area"
- | "base"
- | "br"
- | "col"
- | "embed"
- | "hr"
- | "img"
- | "input"
- | "link"
- | "meta"
- | "source"
- | "track"
- | "wbr"
- )
-}
-
-fn is_html_processor_void_element(name: &str) -> bool {
- matches!(
- name,
- "area"
- | "base"
- | "basefont"
- | "bgsound"
- | "br"
- | "col"
- | "embed"
- | "frame"
- | "hr"
- | "img"
- | "input"
- | "keygen"
- | "link"
- | "meta"
- | "param"
- | "source"
- | "track"
- | "wbr"
- )
-}
-
-fn is_html_processor_special_element(name: &str) -> bool {
- matches!(
- name,
- "address"
- | "applet"
- | "area"
- | "article"
- | "aside"
- | "base"
- | "basefont"
- | "bgsound"
- | "blockquote"
- | "body"
- | "br"
- | "button"
- | "caption"
- | "center"
- | "col"
- | "colgroup"
- | "dd"
- | "details"
- | "dir"
- | "div"
- | "dl"
- | "dt"
- | "embed"
- | "fieldset"
- | "figcaption"
- | "figure"
- | "footer"
- | "form"
- | "frame"
- | "frameset"
- | "h1"
- | "h2"
- | "h3"
- | "h4"
- | "h5"
- | "h6"
- | "head"
- | "header"
- | "hgroup"
- | "hr"
- | "html"
- | "iframe"
- | "img"
- | "input"
- | "keygen"
- | "li"
- | "link"
- | "listing"
- | "main"
- | "marquee"
- | "menu"
- | "meta"
- | "nav"
- | "noembed"
- | "noframes"
- | "noscript"
- | "object"
- | "ol"
- | "p"
- | "param"
- | "plaintext"
- | "pre"
- | "script"
- | "search"
- | "section"
- | "select"
- | "source"
- | "style"
- | "summary"
- | "table"
- | "tbody"
- | "td"
- | "template"
- | "textarea"
- | "tfoot"
- | "th"
- | "thead"
- | "title"
- | "tr"
- | "track"
- | "ul"
- | "wbr"
- | "xmp"
- )
-}
-
-fn is_html_raw_text_element(name: &str) -> bool {
- matches!(
- name,
- "iframe" | "noembed" | "noframes" | "script" | "style" | "textarea" | "title" | "xmp"
- )
-}
-
-fn find_html_closing_tag(bytes: &[u8], name: &str, start: usize) -> Option {
- let needle = name.as_bytes();
- let mut cursor = start;
-
- while cursor + needle.len() + 2 <= bytes.len() {
- let open = find_byte(bytes, b'<', cursor)?;
-
- let name_start = open + 2;
- let name_end = name_start + needle.len();
- if open + 1 < bytes.len()
- && bytes[open + 1] == b'/'
- && name_end <= bytes.len()
- && bytes[name_start..name_end].eq_ignore_ascii_case(needle)
- && is_html_tag_name_boundary(bytes, name_end)
- {
- return Some(open);
- }
-
- cursor = open + 1;
- }
-
- None
-}
-
-fn is_html_tag_name_boundary(bytes: &[u8], cursor: usize) -> bool {
- cursor >= bytes.len()
- || bytes[cursor].is_ascii_whitespace()
- || matches!(bytes[cursor], b'>' | b'/')
-}
-
-fn should_synthesize_html_implied_closer(name: &str, breadcrumbs: &[String]) -> bool {
- let Some(last_open) = breadcrumbs.last() else {
- return false;
- };
-
- if last_open == "P" && html_start_tag_closes_p(name) {
- return true;
- }
-
- if is_html_heading_name(last_open) && is_html_heading_start_tag(name) {
- return true;
- }
-
- if name == "button" && breadcrumbs.iter().any(|open| open == "BUTTON") {
- return breadcrumbs.len() > 2;
- }
-
- if name == "nobr" && breadcrumbs.iter().any(|open| open == "NOBR") {
- return breadcrumbs.len() > 2;
- }
-
- matches!(
- (name, last_open.as_str()),
- ("li", "LI")
- | ("option", "OPTION")
- | ("p", "P")
- | ("rt", "RT")
- | ("rt", "RP")
- | ("rp", "RT")
- | ("rp", "RP")
- | ("optgroup", "OPTION")
- | ("optgroup", "OPTGROUP")
- | ("dt", "DT")
- | ("dt", "DD")
- | ("dd", "DT")
- | ("dd", "DD")
- | ("caption", "CAPTION")
- | ("caption", "COLGROUP")
- | ("caption", "TD")
- | ("caption", "TH")
- | ("caption", "TR")
- | ("caption", "TBODY")
- | ("caption", "THEAD")
- | ("caption", "TFOOT")
- | ("col", "CAPTION")
- | ("colgroup", "CAPTION")
- | ("colgroup", "TD")
- | ("colgroup", "TH")
- | ("colgroup", "TR")
- | ("colgroup", "TBODY")
- | ("colgroup", "THEAD")
- | ("colgroup", "TFOOT")
- | ("tbody", "CAPTION")
- | ("tbody", "TD")
- | ("tbody", "TH")
- | ("tbody", "TR")
- | ("tbody", "TBODY")
- | ("tbody", "THEAD")
- | ("tbody", "TFOOT")
- | ("thead", "CAPTION")
- | ("thead", "TD")
- | ("thead", "TH")
- | ("thead", "TR")
- | ("thead", "TBODY")
- | ("thead", "THEAD")
- | ("thead", "TFOOT")
- | ("tfoot", "CAPTION")
- | ("tfoot", "TD")
- | ("tfoot", "TH")
- | ("tfoot", "TR")
- | ("tfoot", "TBODY")
- | ("tfoot", "THEAD")
- | ("tfoot", "TFOOT")
- | ("tr", "CAPTION")
- | ("td", "CAPTION")
- | ("th", "CAPTION")
- | ("tr", "COLGROUP")
- | ("td", "COLGROUP")
- | ("th", "COLGROUP")
- | ("tbody", "COLGROUP")
- | ("thead", "COLGROUP")
- | ("tfoot", "COLGROUP")
- | ("hr", "OPTION")
- | ("input", "OPTION")
- | ("input", "SELECT")
- | ("textarea", "OPTION")
- | ("textarea", "SELECT")
- | ("td", "TD")
- | ("td", "TH")
- | ("th", "TD")
- | ("th", "TH")
- | ("tr", "TD")
- | ("tr", "TH")
- | ("tr", "TR")
- )
-}
-
-fn is_html_heading_name(name: &str) -> bool {
- matches!(name, "H1" | "H2" | "H3" | "H4" | "H5" | "H6")
-}
-
-fn is_html_heading_start_tag(name: &str) -> bool {
- matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
-}
-
-fn html_start_tag_closes_p(name: &str) -> bool {
- matches!(
- name,
- "address"
- | "article"
- | "aside"
- | "blockquote"
- | "details"
- | "dialog"
- | "dir"
- | "div"
- | "dl"
- | "fieldset"
- | "figcaption"
- | "figure"
- | "footer"
- | "form"
- | "h1"
- | "h2"
- | "h3"
- | "h4"
- | "h5"
- | "h6"
- | "header"
- | "hgroup"
- | "hr"
- | "listing"
- | "main"
- | "menu"
- | "nav"
- | "ol"
- | "p"
- | "pre"
- | "search"
- | "section"
- | "table"
- | "ul"
- )
-}
-
-fn should_synthesize_html_implied_colgroup(name: &str, breadcrumbs: &[String]) -> bool {
- name == "col" && breadcrumbs.last().map(|last| last.as_str()) == Some("TABLE")
-}
-
-fn should_synthesize_html_implied_table_body(name: &str, breadcrumbs: &[String]) -> bool {
- matches!(name, "tr" | "td" | "th")
- && breadcrumbs.last().map(|last| last.as_str()) == Some("TABLE")
-}
-
-fn should_synthesize_html_implied_table_row(name: &str, breadcrumbs: &[String]) -> bool {
- matches!(name, "td" | "th")
- && matches!(
- breadcrumbs.last().map(|last| last.as_str()),
- Some("TBODY" | "THEAD" | "TFOOT")
- )
-}
-
-fn should_synthesize_html_table_form_closer(name: &str, breadcrumbs: &[String]) -> bool {
- matches!(
- name,
- "caption" | "col" | "colgroup" | "tbody" | "thead" | "tfoot" | "tr" | "td" | "th"
- ) && breadcrumbs.len() >= 4
- && breadcrumbs.last().map(|last| last.as_str()) == Some("FORM")
- && breadcrumbs
- .get(breadcrumbs.len().saturating_sub(2))
- .map(|ancestor| ancestor.as_str())
- == Some("TABLE")
-}
-
-fn is_html_table_form_context(breadcrumbs: &[String]) -> bool {
- breadcrumbs.len() >= 4
- && breadcrumbs.last().map(|last| last.as_str()) == Some("FORM")
- && breadcrumbs
- .get(breadcrumbs.len().saturating_sub(2))
- .map(|ancestor| ancestor.as_str())
- == Some("TABLE")
-}
-
-fn should_abort_html_table_child_start(name: &str, breadcrumbs: &[String]) -> bool {
- breadcrumbs.last().map(|last| last.as_str()) == Some("TABLE")
- && !matches!(
- name,
- "caption"
- | "col"
- | "colgroup"
- | "form"
- | "script"
- | "style"
- | "tbody"
- | "td"
- | "template"
- | "tfoot"
- | "th"
- | "thead"
- | "tr"
- )
-}
-
-fn should_abort_html_table_child_closer(name: &str, breadcrumbs: &[String]) -> bool {
- breadcrumbs.last().map(|last| last.as_str()) == Some("TABLE")
- && !matches!(
- name,
- "caption"
- | "colgroup"
- | "table"
- | "tbody"
- | "td"
- | "template"
- | "tfoot"
- | "th"
- | "thead"
- | "tr"
- )
-}
-
-fn should_synthesize_html_implied_closer_before_closing(
- closing_name: &str,
- breadcrumbs: &[String],
-) -> bool {
- let Some(last_open) = breadcrumbs.last() else {
- return false;
- };
-
- if closing_name == "BUTTON" && last_open != "BUTTON" {
- return breadcrumbs.iter().any(|open| open == "BUTTON");
- }
-
- if closing_name == "NOBR" && last_open != "NOBR" {
- return breadcrumbs.iter().any(|open| open == "NOBR");
- }
-
- matches!(
- (last_open.as_str(), closing_name),
- ("LI", "UL")
- | ("LI", "OL")
- | ("OPTION", "SELECT")
- | ("OPTION", "OPTGROUP")
- | ("OPTGROUP", "SELECT")
- | ("DT", "DL")
- | ("DD", "DL")
- | ("RT", "RUBY")
- | ("RP", "RUBY")
- | ("SUMMARY", "DETAILS")
- | ("CAPTION", "TABLE")
- | ("COLGROUP", "TABLE")
- | ("TD", "TR")
- | ("TH", "TR")
- | ("TD", "TBODY")
- | ("TH", "TBODY")
- | ("TD", "THEAD")
- | ("TH", "THEAD")
- | ("TD", "TFOOT")
- | ("TH", "TFOOT")
- | ("TD", "TABLE")
- | ("TH", "TABLE")
- | ("TR", "TBODY")
- | ("TR", "THEAD")
- | ("TR", "TFOOT")
- | ("TR", "TABLE")
- | ("TBODY", "TABLE")
- | ("THEAD", "TABLE")
- | ("TFOOT", "TABLE")
- )
-}
-
-fn synthesize_html_implied_closer_token(
- source_offset: usize,
- breadcrumbs: &mut Vec,
-) -> Option {
- let name = match breadcrumbs.last().map(|name| name.as_str()) {
- Some("LI") => "li",
- Some("OPTION") => "option",
- Some("OPTGROUP") => "optgroup",
- Some("P") => "p",
- Some("RT") => "rt",
- Some("RP") => "rp",
- Some("DT") => "dt",
- Some("DD") => "dd",
- Some("CAPTION") => "caption",
- Some("COLGROUP") => "colgroup",
- Some("TD") => "td",
- Some("TH") => "th",
- Some("TR") => "tr",
- Some("TBODY") => "tbody",
- Some("THEAD") => "thead",
- Some("TFOOT") => "tfoot",
- Some(_) if breadcrumbs.len() > 2 => "",
- _ => return None,
- };
- let name = if name.is_empty() {
- breadcrumbs
- .last()
- .map(|last| last.to_ascii_lowercase())
- .unwrap_or_default()
- } else {
- name.to_string()
- };
-
- breadcrumbs.pop();
-
- Some(HtmlTag {
- name,
- token_type: "#tag".to_string(),
- closing: true,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: source_offset,
- source_end: source_offset,
- text: String::new(),
- comment_type: None,
- full_comment_text: None,
- breadcrumbs: breadcrumbs.clone(),
- depth: breadcrumbs.len(),
- })
-}
-
-fn synthesize_html_implied_opener_token(
- name: &str,
- source_offset: usize,
- breadcrumbs: &mut Vec,
-) -> HtmlTag {
- let uppercase_name = name.to_ascii_uppercase();
- breadcrumbs.push(uppercase_name.clone());
- let token_breadcrumbs = breadcrumbs.clone();
-
- HtmlTag {
- name: name.to_string(),
- token_type: "#tag".to_string(),
- closing: false,
- attributes: HashMap::new(),
- attribute_order: Vec::new(),
- attribute_name_initials: 0,
- source_start: source_offset,
- source_end: source_offset,
- text: String::new(),
- comment_type: None,
- full_comment_text: None,
- breadcrumbs: token_breadcrumbs,
- depth: breadcrumbs.len(),
- }
-}
-
-fn pop_html_breadcrumb(breadcrumbs: &mut Vec, name: &str) {
- while breadcrumbs.len() > 2 {
- match breadcrumbs.pop() {
- Some(open_name) if open_name == name => break,
- Some(_) => continue,
- None => break,
- }
- }
-}
-
-fn html_breadcrumb_contains_open_element(breadcrumbs: &[String], name: &str) -> bool {
- breadcrumbs
- .iter()
- .skip(2)
- .any(|open_name| open_name == name)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- apply_html_text_removals, find_html_attribute_names_with_prefix_count,
- find_html_attribute_names_with_prefix_string, find_html_attribute_removals,
- find_html_attribute_removals_with_prefix, html_serialize_opening_tag,
- html_source_attribute_items, html_tag_has_self_closing_flag, html_token_compact_summary,
- initial_html_breadcrumbs, parse_html_tags, parse_next_html_token,
- parse_next_plain_html_tag_token, HtmlTextRemoval,
- };
-
- fn collect_processor_tokens(html: &str) -> Vec<(String, bool, String, String)> {
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- tokens
- }
-
- #[test]
- fn parses_html_tag_names_and_attributes() {
- let tags = parse_html_tags(
- "x ",
- );
-
- assert_eq!(5, tags.len());
- assert_eq!("main", tags[0].name);
- assert_eq!(Some(&"7".to_string()), tags[0].attributes.get("data-id"));
- assert_eq!("a", tags[1].name);
- assert_eq!(
- Some(&"/x?one=1&two=2".to_string()),
- tags[1].attributes.get("href")
- );
- assert_eq!(
- Some(&"nav".to_string()),
- tags[1].attributes.get("data-kind")
- );
- assert_eq!("#text", tags[2].token_type);
- assert_eq!("x", tags[2].text);
- assert!(tags[3].closing);
-
- let tags = parse_html_tags("x
");
-
- assert_eq!(Some(&"dot".to_string()), tags[0].attributes.get(".x"));
- assert_eq!(Some(&"at".to_string()), tags[0].attributes.get("@x"));
- assert_eq!(Some(&"ok".to_string()), tags[0].attributes.get("data-x"));
- assert_eq!(
- vec![".x".to_string(), "@x".to_string(), "data-x".to_string()],
- tags[0].attribute_order
- );
-
- let tags = parse_html_tags("x
");
-
- assert_eq!(Some(&String::new()), tags[0].attributes.get("=b"));
- assert_eq!(Some(&String::new()), tags[0].attributes.get("a"));
- assert_eq!(Some(&String::new()), tags[0].attributes.get("=c"));
- assert_eq!(None, tags[0].attributes.get("b"));
- assert_eq!(Some(&"ok".to_string()), tags[0].attributes.get("data-x"));
- assert_eq!(
- vec![
- "=b".to_string(),
- "a".to_string(),
- "=c".to_string(),
- "data-x".to_string()
- ],
- tags[0].attribute_order
- );
- }
-
- #[test]
- fn serializes_opening_tag_attributes_from_source() {
- let html = "One ";
- let tags = parse_html_tags(html);
-
- assert_eq!(
- vec![
- ("href".to_string(), Some("#anchor".to_string())),
- ("v".to_string(), Some("5".to_string())),
- ("enabled".to_string(), None),
- ],
- html_source_attribute_items(html.as_bytes(), tags[0].source_start, tags[0].source_end)
- );
- assert_eq!(
- "",
- html_serialize_opening_tag(html, &tags[0])
- );
- }
-
- #[test]
- fn parses_plain_html_tags_without_decoding_discarded_tokens() {
- let html =
- "text x ";
- let mut offset = 0;
- let mut tags = Vec::new();
-
- while let Some(tag) = parse_next_plain_html_tag_token(html.as_bytes(), &mut offset) {
- tags.push((tag.name, tag.closing, tag.attribute_name_initials, tag.text));
- }
-
- assert_eq!(3, tags.len());
- assert_eq!("main", tags[0].0);
- assert!(!tags[0].1);
- assert_ne!(
- 0,
- tags[0].2 & super::html_attribute_prefix_initial_bit("data-")
- );
- assert_eq!("script", tags[1].0);
- assert_eq!("a < b", tags[1].3);
- assert_eq!("main", tags[2].0);
- assert!(tags[2].1);
- }
-
- #[test]
- fn synthesizes_selected_processor_implied_closers() {
- let html = "";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "ul".to_string(),
- false,
- "HTML/BODY/UL".to_string(),
- "#tag".to_string(),
- ),
- (
- "li".to_string(),
- false,
- "HTML/BODY/UL/LI".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/UL/LI/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "li".to_string(),
- true,
- "HTML/BODY/UL".to_string(),
- "#tag".to_string(),
- ),
- (
- "li".to_string(),
- false,
- "HTML/BODY/UL/LI".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/UL/LI/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "li".to_string(),
- true,
- "HTML/BODY/UL".to_string(),
- "#tag".to_string(),
- ),
- (
- "ul".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = " ";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- false,
- "HTML/BODY/TABLE/FORM".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- false,
- "HTML/BODY/TABLE/FORM".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- false,
- "HTML/BODY/TABLE/FORM".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- false,
- "HTML/BODY/TABLE/FORM".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "caption".to_string(),
- false,
- "HTML/BODY/TABLE/CAPTION".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/CAPTION/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "caption".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "Text";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "div".to_string(),
- false,
- "HTML/BODY/DIV".to_string(),
- "#tag".to_string(),
- ),
- (
- "span".to_string(),
- false,
- "HTML/BODY/DIV/SPAN".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/DIV/SPAN/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "span".to_string(),
- true,
- "HTML/BODY/DIV".to_string(),
- "#tag".to_string(),
- ),
- (
- "div".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = " ";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "a".to_string(),
- false,
- "HTML/BODY/A".to_string(),
- "#tag".to_string(),
- ),
- (
- "img".to_string(),
- false,
- "HTML/BODY/A/IMG".to_string(),
- "#tag".to_string(),
- ),
- (
- "a".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "Text
Block
";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "p".to_string(),
- false,
- "HTML/BODY/P".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/P/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "p".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "div".to_string(),
- false,
- "HTML/BODY/DIV".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/DIV/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "div".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "OneTwo ";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "h1".to_string(),
- false,
- "HTML/BODY/H1".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/H1/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "h1".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "h2".to_string(),
- false,
- "HTML/BODY/H2".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/H2/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "h2".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
-
- let html = " TextInline ";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((
- token.name,
- token.closing,
- token.breadcrumbs.join("/"),
- token.token_type,
- ));
- }
-
- assert_eq!(
- vec![
- (
- "p".to_string(),
- false,
- "HTML/BODY/P".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/P/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "span".to_string(),
- false,
- "HTML/BODY/P/SPAN".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/P/SPAN/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "span".to_string(),
- true,
- "HTML/BODY/P".to_string(),
- "#tag".to_string(),
- ),
- (
- "p".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- tokens
- );
- }
-
- #[test]
- fn handles_table_form_comments_and_unsupported_table_starts() {
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- collect_processor_tokens("
")
- );
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- false,
- "HTML/BODY/TABLE/FORM".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "#comment".to_string(),
- false,
- "HTML/BODY/TABLE/#comment".to_string(),
- "#comment".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- collect_processor_tokens("")
- );
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "caption".to_string(),
- false,
- "HTML/BODY/TABLE/CAPTION".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/CAPTION/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "caption".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- collect_processor_tokens("")
- );
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- collect_processor_tokens("")
- );
-
- assert_eq!(
- vec![(
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- )],
- collect_processor_tokens("")
- );
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "td".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD".to_string(),
- "#tag".to_string(),
- ),
- (
- "#text".to_string(),
- false,
- "HTML/BODY/TABLE/TBODY/TR/TD/#text".to_string(),
- "#text".to_string(),
- ),
- (
- "td".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY/TR".to_string(),
- "#tag".to_string(),
- ),
- (
- "tr".to_string(),
- true,
- "HTML/BODY/TABLE/TBODY".to_string(),
- "#tag".to_string(),
- ),
- (
- "tbody".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "table".to_string(),
- true,
- "HTML/BODY".to_string(),
- "#tag".to_string(),
- ),
- ],
- collect_processor_tokens("")
- );
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- false,
- "HTML/BODY/TABLE/FORM".to_string(),
- "#tag".to_string(),
- ),
- (
- "form".to_string(),
- true,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- ),
- ],
- collect_processor_tokens("")
- );
-
- assert_eq!(
- vec![(
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- )],
- collect_processor_tokens("")
- );
-
- assert_eq!(
- vec![(
- "table".to_string(),
- false,
- "HTML/BODY/TABLE".to_string(),
- "#tag".to_string(),
- )],
- collect_processor_tokens("")
- );
- }
-
- #[test]
- fn subdivides_processor_text_tokens_and_stops_at_table_text() {
- let html = " A B
";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((token.name, token.text, token.breadcrumbs.join("/")));
- }
-
- assert_eq!(
- vec![
- ("p".to_string(), String::new(), "HTML/BODY/P".to_string()),
- (
- "#text".to_string(),
- " ".to_string(),
- "HTML/BODY/P/#text".to_string(),
- ),
- (
- "#text".to_string(),
- "A ".to_string(),
- "HTML/BODY/P/#text".to_string(),
- ),
- ("b".to_string(), String::new(), "HTML/BODY/P/B".to_string()),
- (
- "#text".to_string(),
- "B".to_string(),
- "HTML/BODY/P/B/#text".to_string(),
- ),
- ("b".to_string(), String::new(), "HTML/BODY/P".to_string()),
- ("p".to_string(), String::new(), "HTML/BODY".to_string()),
- ],
- tokens
- );
-
- let html = "
A
";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((token.name, token.text, token.breadcrumbs.join("/")));
- }
-
- assert_eq!(
- vec![
- ("p".to_string(), String::new(), "HTML/BODY/P".to_string()),
- (
- "#text".to_string(),
- " \n ".to_string(),
- "HTML/BODY/P/#text".to_string(),
- ),
- (
- "#text".to_string(),
- "A".to_string(),
- "HTML/BODY/P/#text".to_string(),
- ),
- ("p".to_string(), String::new(), "HTML/BODY".to_string()),
- ],
- tokens
- );
-
- let html = "\0 A\0B
";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((token.name, token.text, token.breadcrumbs.join("/")));
- }
-
- assert_eq!(
- vec![
- ("p".to_string(), String::new(), "HTML/BODY/P".to_string()),
- (
- "#text".to_string(),
- " ".to_string(),
- "HTML/BODY/P/#text".to_string(),
- ),
- (
- "#text".to_string(),
- "AB".to_string(),
- "HTML/BODY/P/#text".to_string(),
- ),
- ("p".to_string(), String::new(), "HTML/BODY".to_string()),
- ],
- tokens
- );
-
- let html = "\r\n\tA\rB
";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((token.name, token.text, token.breadcrumbs.join("/")));
- }
-
- assert_eq!(
- vec![
- ("p".to_string(), String::new(), "HTML/BODY/P".to_string()),
- (
- "#text".to_string(),
- "\n\t".to_string(),
- "HTML/BODY/P/#text".to_string(),
- ),
- (
- "#text".to_string(),
- "A\nB".to_string(),
- "HTML/BODY/P/#text".to_string(),
- ),
- ("p".to_string(), String::new(), "HTML/BODY".to_string()),
- ],
- tokens
- );
-
- let html = "";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((token.name, token.text, token.breadcrumbs.join("/")));
- }
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- String::new(),
- "HTML/BODY/TABLE".to_string(),
- ),
- (
- "#text".to_string(),
- " ".to_string(),
- "HTML/BODY/TABLE/#text".to_string(),
- ),
- ],
- tokens
- );
-
- let html = "";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let mut tokens = Vec::new();
-
- while let Some(token) = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- true,
- true,
- true,
- false,
- ) {
- tokens.push((token.name, token.text, token.breadcrumbs.join("/")));
- }
-
- assert_eq!(
- vec![
- (
- "table".to_string(),
- String::new(),
- "HTML/BODY/TABLE".to_string(),
- ),
- (
- "#text".to_string(),
- " ".to_string(),
- "HTML/BODY/TABLE/#text".to_string(),
- ),
- ],
- tokens
- );
- }
-
- #[test]
- fn serializes_attribute_prefix_names_for_extension_bridge() {
- let html = " ";
- let tags = parse_html_tags(html);
-
- assert_eq!(
- Some("data-id\x1fdata-kind".to_string()),
- find_html_attribute_names_with_prefix_string(
- html.as_bytes(),
- tags[0].source_start,
- tags[0].source_end,
- "data-",
- )
- );
- assert_eq!(
- Some(2),
- find_html_attribute_names_with_prefix_count(
- html.as_bytes(),
- tags[0].source_start,
- tags[0].source_end,
- "data-",
- )
- );
-
- let duplicate_html = " ";
- let duplicate_tags = parse_html_tags(duplicate_html);
- assert_eq!(
- Some("data-id\x1fdata-kind".to_string()),
- find_html_attribute_names_with_prefix_string(
- duplicate_html.as_bytes(),
- duplicate_tags[0].source_start,
- duplicate_tags[0].source_end,
- "data-",
- )
- );
- assert_eq!(
- Some(2),
- find_html_attribute_names_with_prefix_count(
- duplicate_html.as_bytes(),
- duplicate_tags[0].source_start,
- duplicate_tags[0].source_end,
- "data-",
- )
- );
-
- assert_eq!(
- Some(String::new()),
- find_html_attribute_names_with_prefix_string(
- html.as_bytes(),
- tags[0].source_start,
- tags[0].source_end,
- "role",
- )
- );
- assert_eq!(
- Some(0),
- find_html_attribute_names_with_prefix_count(
- html.as_bytes(),
- tags[0].source_start,
- tags[0].source_end,
- "role",
- )
- );
- assert!(tags[1].closing);
- assert_eq!(
- None,
- find_html_attribute_names_with_prefix_string(
- html.as_bytes(),
- tags[1].source_start,
- tags[1].source_end,
- "data-",
- )
- );
- assert_eq!(
- None,
- find_html_attribute_names_with_prefix_count(
- html.as_bytes(),
- tags[1].source_start,
- tags[1].source_end,
- "data-",
- )
- );
- }
-
- #[test]
- fn detects_html_self_closing_flags() {
- let html = " ";
- let tags = parse_html_tags(html);
-
- assert_eq!("main", tags[0].name);
- assert!(html_tag_has_self_closing_flag(
- html.as_bytes(),
- tags[0].source_start,
- tags[0].source_end,
- ));
- assert_eq!("img", tags[1].name);
- assert!(!html_tag_has_self_closing_flag(
- html.as_bytes(),
- tags[1].source_start,
- tags[1].source_end,
- ));
- assert_eq!("br", tags[2].name);
- assert!(html_tag_has_self_closing_flag(
- html.as_bytes(),
- tags[2].source_start,
- tags[2].source_end,
- ));
- }
-
- #[test]
- fn records_attribute_initials_without_value_decoding() {
- let html = " ";
- let mut offset = 0;
- let mut breadcrumbs = initial_html_breadcrumbs();
- let tag = parse_next_html_token(
- html.as_bytes(),
- &mut offset,
- &mut breadcrumbs,
- false,
- false,
- false,
- false,
- )
- .expect("expected opening tag");
-
- let data_bit = 1u32 << (b'd' - b'a');
- let aria_bit = 1u32 << (b'a' - b'a');
- let hidden_bit = 1u32 << (b'h' - b'a');
- let class_bit = 1u32 << (b'c' - b'a');
-
- assert_ne!(0, tag.attribute_name_initials & data_bit);
- assert_ne!(0, tag.attribute_name_initials & aria_bit);
- assert_ne!(0, tag.attribute_name_initials & hidden_bit);
- assert_eq!(0, tag.attribute_name_initials & class_bit);
- assert!(tag.attributes.is_empty());
- assert!(tag.attribute_order.is_empty());
- }
-
- #[test]
- fn finds_and_applies_attribute_removals() {
- let html = "Link ";
- let tags = parse_html_tags(html);
- let removals = find_html_attribute_removals(
- html.as_bytes(),
- tags[0].source_start,
- tags[0].source_end,
- "data-id",
- );
-
- assert_eq!(2, removals.len());
- assert_eq!(
- "Link ",
- apply_html_text_removals(
- html,
- &removals
- .iter()
- .map(|(start, length)| HtmlTextRemoval {
- start: *start,
- length: *length,
- replacement: String::new(),
- })
- .collect::>(),
- )
- );
-
- let boolean_html = "Text
";
- let boolean_tags = parse_html_tags(boolean_html);
- let removals = find_html_attribute_removals(
- boolean_html.as_bytes(),
- boolean_tags[0].source_start,
- boolean_tags[0].source_end,
- "disabled",
- );
-
- assert_eq!(1, removals.len());
- assert_eq!(
- "Text
",
- apply_html_text_removals(
- boolean_html,
- &removals
- .iter()
- .map(|(start, length)| HtmlTextRemoval {
- start: *start,
- length: *length,
- replacement: String::new(),
- })
- .collect::>(),
- )
- );
- }
-
- #[test]
- fn finds_and_applies_attribute_removals_with_prefix() {
- let html = "Link ";
- let tags = parse_html_tags(html);
- let (count, removals, names) = find_html_attribute_removals_with_prefix(
- html.as_bytes(),
- tags[0].source_start,
- tags[0].source_end,
- "data-",
- &[],
- )
- .expect("expected opening tag removals");
-
- assert_eq!(2, count);
- assert_eq!(3, removals.len());
- assert_eq!(vec!["data-id".to_string(), "data-track".to_string()], names);
- assert_eq!(
- "Link ",
- apply_html_text_removals(
- html,
- &removals
- .iter()
- .map(|(start, length)| HtmlTextRemoval {
- start: *start,
- length: *length,
- replacement: String::new(),
- })
- .collect::>(),
- )
- );
-
- let (count, removals, names) = find_html_attribute_removals_with_prefix(
- html.as_bytes(),
- tags[0].source_start,
- tags[0].source_end,
- "data-",
- &["data-id".to_string(), "data-track".to_string()],
- )
- .expect("expected opening tag removals");
-
- assert_eq!(0, count);
- assert!(removals.is_empty());
- assert!(names.is_empty());
-
- let boolean_html = "Text
";
- let boolean_tags = parse_html_tags(boolean_html);
- let (count, removals, names) = find_html_attribute_removals_with_prefix(
- boolean_html.as_bytes(),
- boolean_tags[0].source_start,
- boolean_tags[0].source_end,
- "d",
- &[],
- )
- .expect("expected opening tag removals");
-
- assert_eq!(3, count);
- assert_eq!(3, removals.len());
- assert_eq!(
- vec![
- "data-x".to_string(),
- "disabled".to_string(),
- "data-y".to_string()
- ],
- names
- );
- assert_eq!(
- "Text
",
- apply_html_text_removals(
- boolean_html,
- &removals
- .iter()
- .map(|(start, length)| HtmlTextRemoval {
- start: *start,
- length: *length,
- replacement: String::new(),
- })
- .collect::>(),
- )
- );
- }
-
- #[test]
- fn decodes_html_attribute_character_references() {
- let tags = parse_html_tags(
- "x ",
- );
- let expected = format!(
- "A&B/C/D\"{}{}{}{}{}{}",
- '\u{00a0}', '\u{00a9}', '\u{00ae}', '\u{2026}', '\u{2014}', '\u{2209}'
- );
-
- assert_eq!(Some(&expected), tags[0].attributes.get("title"));
- }
-
- #[test]
- fn decodes_numeric_and_legacy_html_character_references() {
- let tags = parse_html_tags(
- "¬in &x ©x ",
- );
- let expected_attribute = format!(
- "{} {} {} {} A {} ¬in &x ¬it;",
- '\u{fffd}', '\u{fffd}', '\u{20ac}', '\u{2026}', '\u{00a9}'
- );
- let expected_title = format!("{}in &x {}x {}", '\u{00ac}', '\u{00a9}', '\u{fffd}');
-
- assert_eq!(Some(&expected_attribute), tags[0].attributes.get("title"));
- assert_eq!("title", tags[2].name);
- assert_eq!(expected_title, tags[2].text);
- }
-
- #[test]
- fn records_opening_and_closing_tags_for_native_cursors() {
- let tags = parse_html_tags("");
-
- assert_eq!(5, tags.len());
- assert_eq!("section", tags[0].name);
- assert!(!tags[0].closing);
- assert_eq!("p", tags[1].name);
- assert!(!tags[1].closing);
- assert_eq!("#text", tags[2].token_type);
- assert_eq!("Text", tags[2].text);
- assert_eq!("p", tags[3].name);
- assert!(tags[3].closing);
- assert_eq!("section", tags[4].name);
- assert!(tags[4].closing);
- }
-
- #[test]
- fn records_fragment_breadcrumbs_and_depth_for_processor_tokens() {
- let tags = parse_html_tags("");
-
- assert_eq!(5, tags.len());
- assert_eq!(
- vec![
- "HTML".to_string(),
- "BODY".to_string(),
- "SECTION".to_string()
- ],
- tags[0].breadcrumbs
- );
- assert_eq!(3, tags[0].depth);
- assert_eq!(
- vec![
- "HTML".to_string(),
- "BODY".to_string(),
- "SECTION".to_string(),
- "P".to_string()
- ],
- tags[1].breadcrumbs
- );
- assert_eq!(4, tags[1].depth);
- assert_eq!(
- vec![
- "HTML".to_string(),
- "BODY".to_string(),
- "SECTION".to_string(),
- "P".to_string(),
- "IMG".to_string()
- ],
- tags[2].breadcrumbs
- );
- assert_eq!(5, tags[2].depth);
- assert_eq!(
- vec![
- "HTML".to_string(),
- "BODY".to_string(),
- "SECTION".to_string()
- ],
- tags[3].breadcrumbs
- );
- assert_eq!(3, tags[3].depth);
- assert_eq!(
- vec!["HTML".to_string(), "BODY".to_string()],
- tags[4].breadcrumbs
- );
- assert_eq!(2, tags[4].depth);
- }
-
- #[test]
- fn serializes_compact_html_token_summaries() {
- let tags = parse_html_tags("");
-
- assert_eq!(
- "t\x1fSECTION\x1f0\x1f3\x1fHTML\x1dBODY\x1dSECTION",
- html_token_compact_summary(&tags[0])
- );
- assert_eq!(
- "s\x1f#text\x1f0\x1f5\x1fHTML\x1dBODY\x1dSECTION\x1dP\x1d#text",
- html_token_compact_summary(&tags[2])
- );
- assert_eq!(
- "t\x1fP\x1f1\x1f3\x1fHTML\x1dBODY\x1dSECTION",
- html_token_compact_summary(&tags[3])
- );
- }
-
- #[test]
- fn records_text_and_comment_tokens() {
- let tags = parse_html_tags("HelloWorld
");
-
- assert_eq!(7, tags.len());
- assert_eq!("#tag", tags[0].token_type);
- assert_eq!("#text", tags[1].token_type);
- assert_eq!("Hello", tags[1].text);
- assert_eq!(
- vec![
- "HTML".to_string(),
- "BODY".to_string(),
- "P".to_string(),
- "#text".to_string()
- ],
- tags[1].breadcrumbs
- );
- assert_eq!("#comment", tags[2].token_type);
- assert_eq!("note", tags[2].text);
- assert_eq!(
- Some("COMMENT_AS_HTML_COMMENT".to_string()),
- tags[2].comment_type
- );
- assert_eq!(Some("note".to_string()), tags[2].full_comment_text);
- assert_eq!(
- vec![
- "HTML".to_string(),
- "BODY".to_string(),
- "P".to_string(),
- "#comment".to_string()
- ],
- tags[2].breadcrumbs
- );
- assert_eq!("#text", tags[4].token_type);
- assert_eq!("World", tags[4].text);
-
- let tags = parse_html_tags("x
");
-
- assert_eq!(4, tags.len());
- assert_eq!("#comment", tags[0].token_type);
- assert_eq!("note", tags[0].text);
- assert_eq!(
- Some("COMMENT_AS_HTML_COMMENT".to_string()),
- tags[0].comment_type
- );
- assert_eq!(Some("note".to_string()), tags[0].full_comment_text);
- assert_eq!("p", tags[1].name);
-
- let tags = parse_html_tags("x
");
-
- assert_eq!(5, tags.len());
- assert_eq!("#comment", tags[0].token_type);
- assert_eq!("", tags[0].text);
- assert_eq!(
- Some("COMMENT_AS_ABRUPTLY_CLOSED_COMMENT".to_string()),
- tags[0].comment_type
- );
- assert_eq!(Some("".to_string()), tags[0].full_comment_text);
- assert_eq!("#comment", tags[1].token_type);
- assert_eq!("", tags[1].text);
- assert_eq!(
- Some("COMMENT_AS_ABRUPTLY_CLOSED_COMMENT".to_string()),
- tags[1].comment_type
- );
- assert_eq!(Some("".to_string()), tags[1].full_comment_text);
- assert_eq!("p", tags[2].name);
- }
-}
diff --git a/extensions/native-apis/src/lib.rs b/extensions/native-apis/src/lib.rs
deleted file mode 100644
index 91ff052f7..000000000
--- a/extensions/native-apis/src/lib.rs
+++ /dev/null
@@ -1,43 +0,0 @@
-#![cfg_attr(windows, feature(abi_vectorcall))]
-
-#[cfg(feature = "php-extension")]
-use ext_php_rs::prelude::*;
-#[cfg(feature = "php-extension")]
-use ext_php_rs::zend::ModuleEntry;
-#[cfg(feature = "php-extension")]
-use ext_php_rs::{info_table_end, info_table_row, info_table_start};
-
-mod html;
-mod url_text;
-mod xml;
-
-#[cfg(feature = "php-extension")]
-extern "C" fn php_module_info(_module: *mut ModuleEntry) {
- info_table_start!();
- info_table_row!("wp_native_apis", "enabled");
- info_table_row!("html", "registered");
- info_table_row!(
- "url_text",
- "registered under WordPress\\DataLiberation\\URL\\NativeURLInTextProcessor"
- );
- info_table_row!("xml", "registered");
- info_table_end!();
-}
-
-#[cfg(feature = "php-extension")]
-#[php_function]
-pub fn wp_native_apis_extension_version() -> &'static str {
- env!("CARGO_PKG_VERSION")
-}
-
-#[cfg(feature = "php-extension")]
-#[php_module]
-pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
- module
- .class::()
- .class::()
- .class::()
- .class::()
- .function(wrap_function!(wp_native_apis_extension_version))
- .info_function(php_module_info)
-}
diff --git a/extensions/native-apis/src/url_text.rs b/extensions/native-apis/src/url_text.rs
deleted file mode 100644
index 1ba04d223..000000000
--- a/extensions/native-apis/src/url_text.rs
+++ /dev/null
@@ -1,654 +0,0 @@
-#![cfg_attr(not(feature = "php-extension"), allow(dead_code))]
-
-#[cfg(feature = "php-extension")]
-use ext_php_rs::{
- prelude::*,
- types::{ZendCallable, Zval},
-};
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct UrlTextCandidate {
- pub raw_url: String,
- pub preprocessed_url: String,
- pub starts_at: usize,
- pub length: usize,
- pub had_protocol: bool,
- pub did_prepend_protocol: bool,
-}
-
-#[cfg(feature = "php-extension")]
-#[php_class]
-#[php(name = "WordPress\\DataLiberation\\URL\\NativeURLInTextProcessor")]
-pub struct NativeUrlInTextProcessor {
- text: String,
- bytes_already_parsed: usize,
- current: Option,
- replacements: Vec,
- validate_urls: bool,
- base_url: Option,
- base_protocol: Option,
-}
-
-#[derive(Clone, Debug)]
-struct UrlTextReplacement {
- start: usize,
- length: usize,
- text: String,
-}
-
-#[cfg(feature = "php-extension")]
-#[php_impl]
-#[php(change_method_case = "snake_case")]
-impl NativeUrlInTextProcessor {
- #[php(optional = base_url)]
- pub fn __construct(text: String, base_url: Option) -> Self {
- let base_protocol = base_url.as_deref().and_then(parse_url_scheme);
-
- Self {
- text,
- bytes_already_parsed: 0,
- current: None,
- replacements: Vec::new(),
- validate_urls: true,
- base_url,
- base_protocol,
- }
- }
-
- pub fn supports_public_api() -> bool {
- true
- }
-
- pub fn use_url_validation(&mut self) {
- self.validate_urls = true;
- }
-
- pub fn set_base_url(&mut self, base_url: String) {
- self.base_protocol = parse_url_scheme(&base_url);
- self.base_url = Some(base_url);
- }
-
- pub fn next_url(&mut self) -> bool {
- self.current = None;
-
- while let Some(mut candidate) =
- find_next_url_text_candidate(&self.text, self.bytes_already_parsed)
- {
- self.bytes_already_parsed = candidate.starts_at + candidate.length;
-
- if self.validate_urls
- && !validate_url_text_candidate(&mut candidate, self.base_protocol.as_deref())
- {
- continue;
- }
-
- self.current = Some(candidate);
- return true;
- }
-
- false
- }
-
- pub fn get_raw_url(&self) -> Option {
- self.current
- .as_ref()
- .map(|candidate| candidate.raw_url.clone())
- }
-
- pub fn get_preprocessed_url(&self) -> Option {
- self.current
- .as_ref()
- .map(|candidate| candidate.preprocessed_url.clone())
- }
-
- pub fn get_parsed_url(&self) -> Zval {
- let Some(candidate) = self.current.as_ref() else {
- return url_zval_bool(false);
- };
-
- let Ok(callable) =
- ZendCallable::try_from_name("WordPress\\DataLiberation\\URL\\WPURL::parse")
- else {
- return url_zval_bool(false);
- };
-
- let result = match self.base_url.as_ref() {
- Some(base_url) => callable.try_call(vec![&candidate.preprocessed_url, base_url]),
- None => callable.try_call(vec![&candidate.preprocessed_url]),
- };
-
- match result {
- Ok(value) if !value.is_false() && !value.is_null() => value,
- _ => url_zval_bool(false),
- }
- }
-
- pub fn get_url_starts_at(&self) -> Option {
- self.current
- .as_ref()
- .map(|candidate| candidate.starts_at as i64)
- }
-
- pub fn get_url_length(&self) -> Option {
- self.current
- .as_ref()
- .map(|candidate| candidate.length as i64)
- }
-
- pub fn had_protocol(&self) -> Option {
- self.current
- .as_ref()
- .map(|candidate| candidate.had_protocol)
- }
-
- pub fn did_prepend_protocol(&self) -> Option {
- self.current
- .as_ref()
- .map(|candidate| candidate.did_prepend_protocol)
- }
-
- pub fn set_raw_url(&mut self, new_url: String) -> bool {
- let Some(candidate) = self.current.as_mut() else {
- return false;
- };
-
- if let Some(replacement) = self
- .replacements
- .iter_mut()
- .find(|replacement| replacement.start == candidate.starts_at)
- {
- replacement.length = candidate.length;
- replacement.text = new_url.clone();
- } else {
- self.replacements.push(UrlTextReplacement {
- start: candidate.starts_at,
- length: candidate.length,
- text: new_url.clone(),
- });
- }
- candidate.raw_url = new_url;
- true
- }
-
- pub fn get_updated_text(&mut self) -> String {
- if self.replacements.is_empty() {
- return self.text.clone();
- }
-
- self.replacements
- .sort_by(|left, right| left.start.cmp(&right.start));
-
- let mut output = String::with_capacity(self.text.len());
- let mut copied = 0;
- for replacement in &self.replacements {
- if replacement.start < copied {
- continue;
- }
-
- output.push_str(&self.text[copied..replacement.start]);
- output.push_str(&replacement.text);
-
- if replacement.start < self.bytes_already_parsed {
- let old_end = replacement.start + replacement.length;
- let old_cursor_delta = self.bytes_already_parsed.saturating_sub(old_end);
- self.bytes_already_parsed = output.len() + old_cursor_delta;
- }
-
- if let Some(current) = self.current.as_mut() {
- if current.starts_at == replacement.start {
- current.starts_at = output.len() - replacement.text.len();
- current.length = replacement.text.len();
- }
- }
-
- copied = replacement.start + replacement.length;
- }
-
- output.push_str(&self.text[copied..]);
- self.text = output;
- self.replacements.clear();
-
- self.text.clone()
- }
-}
-
-pub fn find_next_url_text_candidate(text: &str, offset: usize) -> Option {
- let bytes = text.as_bytes();
- let mut cursor = offset.min(bytes.len());
-
- while cursor < bytes.len() {
- if !is_url_left_boundary(bytes, cursor) {
- cursor += 1;
- continue;
- }
-
- if let Some(candidate) = parse_url_text_candidate_at(text, cursor) {
- return Some(candidate);
- }
-
- cursor += 1;
- }
-
- None
-}
-
-fn parse_url_text_candidate_at(text: &str, start: usize) -> Option {
- let bytes = text.as_bytes();
- let mut had_protocol = false;
- let mut host_start = start;
-
- if ascii_starts_with(bytes, start, b"https:") {
- had_protocol = true;
- host_start = start + 6;
- } else if ascii_starts_with(bytes, start, b"http:") {
- had_protocol = true;
- host_start = start + 5;
- } else if start + 2 <= bytes.len() && &bytes[start..start + 2] == b"//" {
- if start > 0 && bytes[start - 1] == b':' {
- return None;
- }
- host_start = start + 2;
- }
-
- if had_protocol {
- while host_start < bytes.len() && bytes[host_start] == b'/' {
- host_start += 1;
- }
- }
-
- if host_start >= bytes.len() {
- return None;
- }
-
- let mut host_end = host_start;
- while host_end < bytes.len() && is_hostish_byte(bytes[host_end]) {
- host_end += 1;
- }
-
- if host_end <= host_start || !candidate_host_has_url_shape(&text[host_start..host_end]) {
- return None;
- }
-
- let mut end = find_candidate_end(bytes, host_end);
- let trimmed_end = trim_candidate_end(bytes, start, end);
- if trimmed_end <= start {
- return None;
- }
-
- if let Some(port_colon) = malformed_port_colon(bytes, host_end, end) {
- end = port_colon;
- }
-
- let display_end = trim_candidate_end(bytes, start, end);
- if display_end <= start {
- return None;
- }
-
- Some(UrlTextCandidate {
- raw_url: text[start..display_end].to_string(),
- preprocessed_url: text[start..display_end].to_string(),
- starts_at: start,
- length: display_end - start,
- had_protocol: had_protocol || start + 2 <= bytes.len() && &bytes[start..start + 2] == b"//",
- did_prepend_protocol: false,
- })
-}
-
-fn validate_url_text_candidate(
- candidate: &mut UrlTextCandidate,
- base_protocol: Option<&str>,
-) -> bool {
- let mut preprocessed_url = candidate.raw_url.clone();
- if !candidate.had_protocol {
- let Some(protocol) = base_protocol else {
- return false;
- };
-
- if !is_http_or_https_scheme(protocol) {
- return false;
- }
-
- preprocessed_url = format!("{protocol}://{}", candidate.raw_url);
- candidate.did_prepend_protocol = true;
- } else if preprocessed_url.starts_with("//") {
- let Some(protocol) = base_protocol else {
- return false;
- };
-
- if !is_http_or_https_scheme(protocol) {
- return false;
- }
- } else if !starts_with_http_or_https_scheme(&preprocessed_url) {
- return false;
- }
-
- if has_authority_auth_details(&preprocessed_url) {
- return false;
- }
-
- if has_invalid_authority_port(&preprocessed_url) {
- return false;
- }
-
- if !candidate.had_protocol {
- let Some(hostname) = candidate_hostname(&candidate.raw_url) else {
- return false;
- };
-
- let Some(last_dot) = hostname.rfind('.') else {
- return false;
- };
-
- if !is_known_public_domain(&hostname[last_dot + 1..]) {
- return false;
- }
- }
-
- candidate.preprocessed_url = preprocessed_url;
- true
-}
-
-#[cfg(feature = "php-extension")]
-fn url_zval_bool(value: bool) -> Zval {
- let mut zval = Zval::new();
- zval.set_bool(value);
- zval
-}
-
-fn parse_url_scheme(url: &str) -> Option {
- let colon = url.find(':')?;
- let first_delimiter = url
- .find(|character| matches!(character, '/' | '?' | '#'))
- .unwrap_or(url.len());
- if colon > first_delimiter {
- return None;
- }
-
- Some(url[..colon].to_ascii_lowercase())
-}
-
-fn is_http_or_https_scheme(scheme: &str) -> bool {
- scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
-}
-
-fn starts_with_http_or_https_scheme(url: &str) -> bool {
- ascii_starts_with(url.as_bytes(), 0, b"http:")
- || ascii_starts_with(url.as_bytes(), 0, b"https:")
-}
-
-fn authority_range(url: &str) -> Option<(usize, usize)> {
- let bytes = url.as_bytes();
- let authority_start = if bytes.starts_with(b"//") {
- 2
- } else if ascii_starts_with(bytes, 0, b"http://") {
- 7
- } else if ascii_starts_with(bytes, 0, b"https://") {
- 8
- } else {
- return None;
- };
-
- let authority_end = bytes[authority_start..]
- .iter()
- .position(|byte| matches!(*byte, b'/' | b'?' | b'#'))
- .map(|offset| authority_start + offset)
- .unwrap_or(bytes.len());
-
- Some((authority_start, authority_end))
-}
-
-fn has_authority_auth_details(url: &str) -> bool {
- let Some((start, end)) = authority_range(url) else {
- return false;
- };
-
- url.as_bytes()[start..end].contains(&b'@')
-}
-
-fn has_invalid_authority_port(url: &str) -> bool {
- let Some((start, end)) = authority_range(url) else {
- return false;
- };
-
- let authority = &url[start..end];
- if authority.starts_with('[') {
- return authority.find(']').is_none();
- }
-
- let Some(colon) = authority.rfind(':') else {
- return false;
- };
-
- let port = &authority[colon + 1..];
- !port.is_empty()
- && port.bytes().all(|byte| byte.is_ascii_digit())
- && port.parse::().is_err()
-}
-
-fn candidate_hostname(raw_url: &str) -> Option<&str> {
- let bytes = raw_url.as_bytes();
- let mut start = 0;
- if bytes.starts_with(b"//") {
- start = 2;
- } else if ascii_starts_with(bytes, 0, b"http:") {
- start = 5;
- while start < bytes.len() && bytes[start] == b'/' {
- start += 1;
- }
- } else if ascii_starts_with(bytes, 0, b"https:") {
- start = 6;
- while start < bytes.len() && bytes[start] == b'/' {
- start += 1;
- }
- }
-
- let end = bytes[start..]
- .iter()
- .position(|byte| !is_hostish_byte(*byte))
- .map(|offset| start + offset)
- .unwrap_or(bytes.len());
- if end <= start {
- return None;
- }
-
- Some(&raw_url[start..end])
-}
-
-fn find_candidate_end(bytes: &[u8], mut cursor: usize) -> usize {
- while cursor < bytes.len() {
- let byte = bytes[cursor];
- if byte <= b' ' || byte == b'<' || byte == b'>' {
- break;
- }
- cursor += 1;
- }
-
- cursor
-}
-
-fn malformed_port_colon(bytes: &[u8], host_end: usize, candidate_end: usize) -> Option {
- if host_end >= candidate_end || bytes[host_end] != b':' {
- return None;
- }
-
- let mut cursor = host_end + 1;
- let digits_start = cursor;
- while cursor < candidate_end && bytes[cursor].is_ascii_digit() {
- cursor += 1;
- }
-
- let digit_count = cursor - digits_start;
- if digit_count == 0 || digit_count > 5 {
- return Some(host_end);
- }
-
- None
-}
-
-fn trim_candidate_end(bytes: &[u8], start: usize, mut end: usize) -> usize {
- while end > start && is_trailing_url_punctuation(bytes[end - 1]) {
- end -= 1;
- }
-
- end
-}
-
-fn is_trailing_url_punctuation(byte: u8) -> bool {
- matches!(
- byte,
- b'(' | b'{' | b'[' | b'`' | b'!' | b';' | b':' | b'\'' | b'"' | b'.' | b',' | b'?' | b')'
- )
-}
-
-fn is_url_left_boundary(bytes: &[u8], offset: usize) -> bool {
- if offset == 0 {
- return true;
- }
-
- if offset >= 2 && bytes[offset - 1] == b'/' && bytes[offset - 2] == b'/' {
- return false;
- }
-
- let previous = bytes[offset - 1];
- !(previous.is_ascii_alphanumeric()
- || previous == b'_'
- || previous == b'-'
- || previous == b'.'
- || previous == b'@')
-}
-
-fn candidate_host_has_url_shape(host: &str) -> bool {
- if host.eq_ignore_ascii_case("localhost") || host.parse::().is_ok() {
- return true;
- }
-
- let Some(last_dot) = host.rfind('.') else {
- return false;
- };
- let tld = &host[last_dot + 1..];
- tld.len() >= 2
- && tld.len() <= 63
- && tld
- .bytes()
- .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
- && host.split('.').all(is_valid_hostname_label)
-}
-
-fn is_known_public_domain(tld: &str) -> bool {
- if tld.eq_ignore_ascii_case("internal") {
- return true;
- }
-
- if tld.is_empty()
- || !tld
- .bytes()
- .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
- {
- return false;
- }
-
- let needle = format!("'{}'", tld.to_ascii_lowercase());
- include_str!("../../../components/DataLiberation/URL/public-suffix-list.php").contains(&needle)
-}
-
-fn is_valid_hostname_label(label: &str) -> bool {
- let bytes = label.as_bytes();
- !bytes.is_empty()
- && bytes.len() <= 63
- && bytes[0] != b'-'
- && bytes[bytes.len() - 1] != b'-'
- && bytes
- .iter()
- .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-' || *byte == b'%')
-}
-
-fn is_hostish_byte(byte: u8) -> bool {
- byte.is_ascii_alphanumeric()
- || byte == b'.'
- || byte == b'-'
- || byte == b'%'
- || byte == b'['
- || byte == b']'
-}
-
-fn ascii_starts_with(bytes: &[u8], offset: usize, needle: &[u8]) -> bool {
- offset + needle.len() <= bytes.len()
- && bytes[offset..offset + needle.len()].eq_ignore_ascii_case(needle)
-}
-
-#[cfg(test)]
-mod tests {
- use super::{find_next_url_text_candidate, validate_url_text_candidate, UrlTextCandidate};
-
- #[test]
- fn finds_http_https_and_bare_domain_candidates() {
- let text = "Visit https://example.com/a?x=1, then example.org/docs.";
- let first = find_next_url_text_candidate(text, 0).expect("first URL");
- assert_eq!("https://example.com/a?x=1", first.raw_url);
- assert_eq!(6, first.starts_at);
-
- let second =
- find_next_url_text_candidate(text, first.starts_at + first.length).expect("second URL");
- assert_eq!("example.org/docs", second.raw_url);
- }
-
- #[test]
- fn trims_common_trailing_punctuation() {
- let text = "See (https://wordpress.org/plugins).";
- let candidate = find_next_url_text_candidate(text, 0).expect("URL");
- assert_eq!("https://wordpress.org/plugins", candidate.raw_url);
- assert_eq!(candidate.raw_url.len(), candidate.length);
- }
-
- #[test]
- fn truncates_malformed_ports_at_the_colon() {
- let text = "Visit http://w.org:/c now";
- let candidate = find_next_url_text_candidate(text, 0).expect("URL");
- assert_eq!("http://w.org", candidate.raw_url);
- }
-
- #[test]
- fn ignores_embedded_protocol_fragments() {
- assert!(find_next_url_text_candidate("ahttp://example.com", 0).is_none());
- }
-
- #[test]
- fn accepts_punycode_tlds() {
- let text = "Visit http://xn--fsqu00a.xn--0zwm56d";
- let candidate = find_next_url_text_candidate(text, 0).expect("URL");
- assert_eq!("http://xn--fsqu00a.xn--0zwm56d", candidate.raw_url);
- }
-
- #[test]
- fn validates_public_url_candidates_with_base_protocol() {
- let mut candidate = find_next_url_text_candidate("Visit example.com/docs", 0).expect("URL");
- assert!(validate_url_text_candidate(&mut candidate, Some("https")));
- assert_eq!("https://example.com/docs", candidate.preprocessed_url);
- assert!(candidate.did_prepend_protocol);
- }
-
- #[test]
- fn rejects_filename_like_bare_domains_with_unknown_tlds() {
- let mut candidate = find_next_url_text_candidate("Edit plugins.php", 0).expect("candidate");
- assert!(!validate_url_text_candidate(&mut candidate, Some("https")));
- }
-
- #[test]
- fn rejects_authority_credentials() {
- let mut candidate = UrlTextCandidate {
- raw_url: "https://user@example.com/path".to_string(),
- preprocessed_url: "https://user@example.com/path".to_string(),
- starts_at: 6,
- length: 29,
- had_protocol: true,
- did_prepend_protocol: false,
- };
- assert!(!validate_url_text_candidate(&mut candidate, Some("https")));
- }
-
- #[test]
- fn rejects_bare_domains_without_base_protocol() {
- let mut candidate = find_next_url_text_candidate("Visit example.com", 0).expect("URL");
- assert!(!validate_url_text_candidate(&mut candidate, None));
- }
-}
diff --git a/extensions/native-apis/src/xml.rs b/extensions/native-apis/src/xml.rs
deleted file mode 100644
index eae7bca7a..000000000
--- a/extensions/native-apis/src/xml.rs
+++ /dev/null
@@ -1,10846 +0,0 @@
-#![cfg_attr(not(feature = "php-extension"), allow(dead_code))]
-
-use std::collections::{HashMap, HashSet};
-use std::fmt::Write;
-use std::rc::Rc;
-
-#[cfg(feature = "php-extension")]
-use ext_php_rs::prelude::*;
-#[cfg(feature = "php-extension")]
-use ext_php_rs::types::{ZendHashTable, Zval};
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct XmlToken {
- pub start_offset: usize,
- pub name: String,
- pub token_type: String,
- pub namespace: Option,
- pub local_name: String,
- pub closing: bool,
- pub empty_element: bool,
- pub attributes: HashMap,
- pub attribute_order: Vec,
- pub text: String,
- pub text_start: usize,
- pub text_end: usize,
- pub breadcrumbs: Vec<(String, String)>,
- pub depth: usize,
-}
-
-const XML_TEXT_RANGE_NONE: usize = usize::MAX;
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct XmlDocument {
- pub tokens: Vec,
- pub error: Option,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-struct XmlStreamState {
- offset: usize,
- stack: Vec,
- context_stack_depth: usize,
- breadcrumb_stack: Vec<(String, String)>,
- namespace_stack: Vec>>,
- root_seen: bool,
- finished: bool,
-}
-
-impl XmlStreamState {
- fn new() -> Self {
- let mut root_namespaces = HashMap::new();
- root_namespaces.insert(
- "xml".to_string(),
- "http://www.w3.org/XML/1998/namespace".to_string(),
- );
-
- Self {
- offset: 0,
- stack: Vec::new(),
- context_stack_depth: 0,
- breadcrumb_stack: Vec::new(),
- namespace_stack: vec![Rc::new(root_namespaces)],
- root_seen: false,
- finished: false,
- }
- }
-}
-
-#[cfg(feature = "php-extension")]
-#[derive(Clone)]
-struct XmlBookmark {
- current: Option,
- stream: Option,
- stream_reentrancy_base_state: Option,
- current_stream_token: Option,
- current_stream_token_start_state: Option,
- exhausted: bool,
- expecting_more_input: bool,
- paused_at_incomplete_input: bool,
- last_error: Option,
- pending_stream_error: Option,
-}
-
-#[derive(Default)]
-struct XmlNextTagQuery {
- breadcrumbs: Option>,
- match_offset: i64,
-}
-
-#[cfg(feature = "php-extension")]
-fn encode_xml_native_cursor(state: &XmlStreamState) -> String {
- format!(
- "WP_NATIVE_XML_CURSOR_V2:{}|{}|{}|{}|{}|{}",
- state.stack.len(),
- if state.root_seen { "1" } else { "0" },
- if state.finished { "1" } else { "0" },
- encode_xml_cursor_string_list(&state.stack),
- encode_xml_cursor_breadcrumbs(&state.breadcrumb_stack),
- encode_xml_cursor_namespace_stack(&state.namespace_stack)
- )
-}
-
-#[cfg(feature = "php-extension")]
-fn decode_xml_native_cursor(cursor: &str) -> Option {
- let payload = cursor.strip_prefix("WP_NATIVE_XML_CURSOR_V2:")?;
- let parts: Vec<&str> = payload.split('|').collect();
- if parts.len() != 6 {
- return None;
- }
-
- let context_stack_depth = parts[0].parse::().ok()?;
- let root_seen = match parts[1] {
- "0" => false,
- "1" => true,
- _ => return None,
- };
- let finished = match parts[2] {
- "0" => false,
- "1" => true,
- _ => return None,
- };
-
- let mut state = XmlStreamState::new();
- state.offset = 0;
- state.context_stack_depth = context_stack_depth;
- state.root_seen = root_seen;
- state.finished = finished;
- state.stack = decode_xml_cursor_string_list(parts[3])?;
- state.breadcrumb_stack = decode_xml_cursor_breadcrumbs(parts[4])?;
- state.namespace_stack = decode_xml_cursor_namespace_stack(parts[5])?;
- if state.namespace_stack.is_empty() {
- state.namespace_stack = XmlStreamState::new().namespace_stack;
- }
- if state.context_stack_depth > state.stack.len() {
- return None;
- }
-
- Some(state)
-}
-
-#[cfg(feature = "php-extension")]
-fn encode_xml_cursor_string_list(values: &[String]) -> String {
- values
- .iter()
- .map(|value| encode_xml_cursor_hex(value.as_bytes()))
- .collect::>()
- .join(",")
-}
-
-#[cfg(feature = "php-extension")]
-fn decode_xml_cursor_string_list(encoded: &str) -> Option> {
- if encoded.is_empty() {
- return Some(Vec::new());
- }
-
- encoded.split(',').map(decode_xml_cursor_hex).collect()
-}
-
-#[cfg(feature = "php-extension")]
-fn encode_xml_cursor_breadcrumbs(values: &[(String, String)]) -> String {
- values
- .iter()
- .map(|(namespace, local_name)| {
- format!(
- "{}={}",
- encode_xml_cursor_hex(namespace.as_bytes()),
- encode_xml_cursor_hex(local_name.as_bytes())
- )
- })
- .collect::>()
- .join(",")
-}
-
-#[cfg(feature = "php-extension")]
-fn decode_xml_cursor_breadcrumbs(encoded: &str) -> Option> {
- if encoded.is_empty() {
- return Some(Vec::new());
- }
-
- encoded
- .split(',')
- .map(|entry| {
- let mut parts = entry.splitn(2, '=');
- let namespace = decode_xml_cursor_hex(parts.next()?)?;
- let local_name = decode_xml_cursor_hex(parts.next()?)?;
- Some((namespace, local_name))
- })
- .collect()
-}
-
-#[cfg(feature = "php-extension")]
-fn encode_xml_cursor_namespace_stack(values: &[Rc>]) -> String {
- values
- .iter()
- .map(|frame| {
- frame
- .iter()
- .map(|(prefix, namespace)| {
- format!(
- "{}={}",
- encode_xml_cursor_hex(prefix.as_bytes()),
- encode_xml_cursor_hex(namespace.as_bytes())
- )
- })
- .collect::>()
- .join(",")
- })
- .collect::>()
- .join(";")
-}
-
-#[cfg(feature = "php-extension")]
-fn decode_xml_cursor_namespace_stack(encoded: &str) -> Option>>> {
- if encoded.is_empty() {
- return Some(Vec::new());
- }
-
- encoded
- .split(';')
- .map(|frame| {
- let mut namespaces = HashMap::new();
- if !frame.is_empty() {
- for entry in frame.split(',') {
- let mut parts = entry.splitn(2, '=');
- let prefix = decode_xml_cursor_hex(parts.next()?)?;
- let namespace = decode_xml_cursor_hex(parts.next()?)?;
- namespaces.insert(prefix, namespace);
- }
- }
- Some(Rc::new(namespaces))
- })
- .collect()
-}
-
-#[cfg(feature = "php-extension")]
-fn encode_xml_cursor_hex(bytes: &[u8]) -> String {
- const HEX: &[u8; 16] = b"0123456789abcdef";
- let mut encoded = String::with_capacity(bytes.len() * 2);
- for byte in bytes {
- encoded.push(HEX[(byte >> 4) as usize] as char);
- encoded.push(HEX[(byte & 0x0f) as usize] as char);
- }
- encoded
-}
-
-#[cfg(feature = "php-extension")]
-fn decode_xml_cursor_hex(encoded: &str) -> Option {
- let bytes = encoded.as_bytes();
- if bytes.len() & 1 != 0 {
- return None;
- }
-
- let mut decoded = Vec::with_capacity(bytes.len() / 2);
- let mut cursor = 0;
- while cursor < bytes.len() {
- let high = xml_cursor_hex_value(bytes[cursor])?;
- let low = xml_cursor_hex_value(bytes[cursor + 1])?;
- decoded.push((high << 4) | low);
- cursor += 2;
- }
-
- String::from_utf8(decoded).ok()
-}
-
-#[cfg(feature = "php-extension")]
-fn xml_cursor_hex_value(byte: u8) -> Option {
- match byte {
- b'0'..=b'9' => Some(byte - b'0'),
- b'a'..=b'f' => Some(byte - b'a' + 10),
- b'A'..=b'F' => Some(byte - b'A' + 10),
- _ => None,
- }
-}
-
-#[cfg(feature = "php-extension")]
-#[php_class]
-#[php(name = "WordPress\\XML\\NativeXMLProcessor")]
-pub struct NativeXmlProcessor {
- source: String,
- document: Option,
- current: Option,
- stream: Option,
- stream_reentrancy_base_state: Option,
- current_stream_token: Option,
- current_stream_token_start_state: Option,
- exhausted: bool,
- expecting_more_input: bool,
- paused_at_incomplete_input: bool,
- last_error: Option,
- pending_stream_error: Option,
- bookmarks: HashMap,
-}
-
-#[cfg(feature = "php-extension")]
-#[php_impl]
-#[php(change_method_case = "snake_case")]
-impl NativeXmlProcessor {
- fn new_from_string(xml: String) -> Self {
- Self {
- source: xml,
- document: None,
- current: None,
- stream: None,
- stream_reentrancy_base_state: None,
- current_stream_token: None,
- current_stream_token_start_state: None,
- exhausted: false,
- expecting_more_input: false,
- paused_at_incomplete_input: false,
- last_error: None,
- pending_stream_error: None,
- bookmarks: HashMap::new(),
- }
- }
-
- pub fn supports_public_api() -> bool {
- true
- }
-
- #[php(optional = cursor)]
- pub fn create_from_string(
- xml: String,
- cursor: Option,
- known_definite_encoding: Option,
- document_namespaces: Option<&Zval>,
- ) -> Option {
- if cursor.is_some()
- || known_definite_encoding.as_deref().unwrap_or("UTF-8") != "UTF-8"
- || document_namespaces.is_some()
- {
- return None;
- }
-
- Some(Self::new_from_string(xml))
- }
-
- pub fn create_for_streaming(
- xml: String,
- cursor: Option,
- known_definite_encoding: String,
- _document_namespaces: &Zval,
- ) -> Option {
- if known_definite_encoding != "UTF-8" {
- return None;
- }
-
- let stream = if let Some(cursor) = cursor {
- if let Some(state) = decode_xml_native_cursor(&cursor) {
- Some(state)
- } else if cursor.starts_with("WP_NATIVE_XML_CURSOR_V1:") {
- None
- } else {
- return None;
- }
- } else {
- None
- };
-
- let mut processor = Self::new_from_string(xml);
- processor.stream_reentrancy_base_state =
- Some(stream.clone().unwrap_or_else(XmlStreamState::new));
- processor.stream = stream;
- processor.expecting_more_input = true;
-
- Some(processor)
- }
-
- pub fn get_reentrancy_cursor(&self) -> String {
- if let Some(state) = self.current_stream_token_start_state.as_ref() {
- return encode_xml_native_cursor(state);
- }
-
- if let Some(state) = self.current_stream_token_start_state() {
- return encode_xml_native_cursor(&state);
- }
-
- let offset = self
- .current_token()
- .map(|token| token.start_offset)
- .unwrap_or(0);
-
- format!("WP_NATIVE_XML_CURSOR_V1:{offset}")
- }
-
- pub fn next_token(&mut self) -> bool {
- if self.exhausted {
- return false;
- }
-
- if self.document.is_none() {
- return self.next_cursor_stream_token();
- }
-
- self.ensure_document();
-
- let next = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before token iteration");
- if next >= document.tokens.len() {
- self.current = None;
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.exhausted = true;
- return false;
- }
-
- self.current = Some(next);
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- true
- }
-
- pub fn next_token_metadata(&mut self) -> Option {
- if !self.next_token() {
- return None;
- }
-
- self.current_token_metadata()
- }
-
- pub fn next_token_summary(&mut self) -> Option {
- if !self.next_token() {
- return None;
- }
-
- self.current_token().map(xml_token_metadata)
- }
-
- pub fn next_token_compact_summary(&mut self) -> Option {
- if !self.next_token() {
- return None;
- }
-
- self.current_token().map(xml_token_compact_summary)
- }
-
- pub fn next_token_compact_summary_batch(&mut self, max_tokens: i64) -> Option {
- let limit = if max_tokens > 0 {
- max_tokens.min(1024) as usize
- } else {
- 64
- };
- let mut summaries = String::new();
- let mut count = 0;
-
- if self.exhausted {
- return None;
- }
-
- if self.document.is_none() {
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return None;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before token iteration");
- while count < limit {
- match parse_next_xml_stream_token(&self.source, state) {
- Ok(Some(token)) => {
- self.paused_at_incomplete_input = false;
- if count > 0 {
- summaries.push('\x1e');
- }
- summaries.push_str(&xml_token_compact_summary(&token));
- self.current_stream_token = Some(token);
- count += 1;
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
- Err(error) => {
- self.current_stream_token = None;
- if self.expecting_more_input && is_incomplete_xml_stream_error(&error) {
- self.paused_at_incomplete_input = true;
- break;
- }
-
- if summaries.is_empty() {
- self.last_error = Some(error);
- self.exhausted = true;
- } else {
- self.pending_stream_error = Some(error);
- }
- break;
- }
- }
- }
-
- return if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- };
- }
-
- while count < limit {
- if !self.next_token() {
- break;
- }
-
- if count > 0 {
- summaries.push('\x1e');
- }
-
- if let Some(token) = self.current_token() {
- summaries.push_str(&xml_token_compact_summary(token));
- }
-
- count += 1;
- }
-
- if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- }
- }
-
- pub fn next_token_fast_compact_summary_batch(&mut self, max_tokens: i64) -> Option {
- let limit = if max_tokens > 0 {
- max_tokens.min(1024) as usize
- } else {
- 64
- };
- let mut summaries = String::new();
- let mut count = 0;
-
- if self.exhausted {
- return None;
- }
-
- if self.document.is_some() {
- return self.next_token_compact_summary_batch(max_tokens);
- }
-
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return None;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before compact token iteration");
- while count < limit {
- match parse_next_xml_stream_compact_summary(&self.source, state) {
- Ok(Some(summary)) => {
- self.paused_at_incomplete_input = false;
- if count > 0 {
- summaries.push('\x1e');
- }
- summaries.push_str(&summary);
- self.current_stream_token = None;
- count += 1;
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
- Err(error) => {
- self.current_stream_token = None;
- if self.expecting_more_input && is_incomplete_xml_stream_error(&error) {
- self.paused_at_incomplete_input = true;
- break;
- }
-
- if summaries.is_empty() {
- self.last_error = Some(error);
- self.exhausted = true;
- } else {
- self.pending_stream_error = Some(error);
- }
- break;
- }
- }
- }
-
- if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- }
- }
-
- pub fn next_token_hot_compact_summary_batch(&mut self, max_tokens: i64) -> Option {
- let limit = if max_tokens > 0 {
- max_tokens.min(1024) as usize
- } else {
- 64
- };
- let mut summaries = String::new();
- let mut count = 0;
-
- if self.exhausted {
- return None;
- }
-
- if self.document.is_some() {
- while count < limit {
- if !self.next_token() {
- break;
- }
-
- if count > 0 {
- summaries.push('\x1e');
- }
-
- if let Some(token) = self.current_token() {
- summaries.push_str(&xml_token_hot_compact_summary(token));
- }
-
- count += 1;
- }
-
- return if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- };
- }
-
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return None;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before compact token iteration");
- while count < limit {
- match parse_next_xml_stream_hot_compact_summary(&self.source, state) {
- Ok(Some(summary)) => {
- self.paused_at_incomplete_input = false;
- if count > 0 {
- summaries.push('\x1e');
- }
- summaries.push_str(&summary);
- self.current_stream_token = None;
- count += 1;
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
- Err(error) => {
- self.current_stream_token = None;
- if self.expecting_more_input && is_incomplete_xml_stream_error(&error) {
- self.paused_at_incomplete_input = true;
- break;
- }
-
- if summaries.is_empty() {
- self.last_error = Some(error);
- self.exhausted = true;
- } else {
- self.pending_stream_error = Some(error);
- }
- break;
- }
- }
- }
-
- if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- }
- }
-
- pub fn next_token_cursor_compact_summary_batch(&mut self, max_tokens: i64) -> Option {
- let limit = if max_tokens > 0 {
- max_tokens.min(1024) as usize
- } else {
- 64
- };
- let mut summaries = String::new();
- let mut count = 0;
-
- if self.exhausted {
- return None;
- }
-
- if self.document.is_some() {
- while count < limit {
- if !self.next_token() {
- break;
- }
-
- if count > 0 {
- summaries.push('\x1e');
- }
-
- if let Some(token) = self.current_token() {
- summaries.push_str(&xml_token_cursor_compact_summary(token));
- }
-
- count += 1;
- }
-
- return if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- };
- }
-
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return None;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before cursor compact token iteration");
- while count < limit {
- match parse_next_xml_stream_cursor_compact_summary(&self.source, state) {
- Ok(Some(summary)) => {
- self.paused_at_incomplete_input = false;
- if count > 0 {
- summaries.push('\x1e');
- }
- summaries.push_str(&summary);
- self.current_stream_token = None;
- count += 1;
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
- Err(error) => {
- self.current_stream_token = None;
- if self.expecting_more_input && is_incomplete_xml_stream_error(&error) {
- self.paused_at_incomplete_input = true;
- break;
- }
- if summaries.is_empty() {
- self.last_error = Some(error);
- self.exhausted = true;
- } else {
- self.pending_stream_error = Some(error);
- }
- break;
- }
- }
- }
-
- if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- }
- }
-
- pub fn next_token_summary_batch(&mut self, max_tokens: i64) -> Vec> {
- let limit = if max_tokens > 0 {
- max_tokens.min(256) as usize
- } else {
- return Vec::new();
- };
- let mut rows = Vec::new();
-
- while rows.len() < limit && self.next_token() {
- if let Some(token) = self.current_token() {
- rows.push(xml_token_public_summary_row(token));
- }
- }
-
- rows
- }
-
- pub fn next_tag_compact_summary_batch(
- &mut self,
- max_tags: i64,
- attribute_name: String,
- ) -> Option {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- 64
- };
- let mut summaries = String::new();
- let mut tag_count = 0;
- let mut token_delta = 0;
-
- if self.exhausted {
- return None;
- }
-
- if self.document.is_none() {
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return None;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before tag iteration");
- while tag_count < limit {
- match parse_next_xml_stream_token(&self.source, state) {
- Ok(Some(token)) => {
- self.paused_at_incomplete_input = false;
- token_delta += 1;
-
- if token.token_type != "#tag" || token.closing {
- self.current_stream_token = Some(token);
- continue;
- }
-
- if tag_count > 0 {
- summaries.push('\x1e');
- }
- summaries.push_str(&xml_tag_compact_summary(
- &token,
- token_delta,
- &attribute_name,
- ));
- self.current_stream_token = Some(token);
- tag_count += 1;
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
- Err(error) => {
- self.current_stream_token = None;
- if self.expecting_more_input && is_incomplete_xml_stream_error(&error) {
- self.paused_at_incomplete_input = true;
- break;
- }
-
- if summaries.is_empty() {
- self.last_error = Some(error);
- self.exhausted = true;
- } else {
- self.pending_stream_error = Some(error);
- }
- break;
- }
- }
- }
-
- return if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- };
- }
-
- while tag_count < limit {
- let next = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before tag iteration");
- if next >= document.tokens.len() {
- self.current = None;
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
-
- self.current = Some(next);
- token_delta += 1;
-
- let token = &document.tokens[next];
- if token.token_type != "#tag" || token.closing {
- continue;
- }
-
- if tag_count > 0 {
- summaries.push('\x1e');
- }
- summaries.push_str(&xml_tag_compact_summary(
- token,
- token_delta,
- &attribute_name,
- ));
- tag_count += 1;
- }
-
- if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- }
- }
-
- pub fn next_tag_summary_batch(
- &mut self,
- max_tags: i64,
- attribute_name: String,
- ) -> Vec> {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- return Vec::new();
- };
- let mut rows = Vec::new();
-
- while rows.len() < limit && self.next_token() {
- let Some(token) = self.current_token() else {
- continue;
- };
-
- if token.token_type != "#tag" || token.closing {
- continue;
- }
-
- rows.push(xml_tag_public_summary_row(token, &attribute_name));
- }
-
- rows
- }
-
- pub fn next_tag_count_batch(
- &mut self,
- max_tags: i64,
- attribute_name: String,
- ) -> Option {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- 64
- };
- let tag_limit = limit as i64;
- let mut summary = XmlTokenStreamSummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- };
-
- if self.exhausted {
- return None;
- }
-
- if self.document.is_none() {
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return None;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before tag count iteration");
- while summary.tag_count < tag_limit {
- match parse_next_xml_stream_token(&self.source, state) {
- Ok(Some(token)) => {
- self.paused_at_incomplete_input = false;
- summary.token_count += 1;
-
- if token.token_type == "#tag" && !token.closing {
- summary.tag_count += 1;
- if token.attributes.contains_key(&attribute_name) {
- summary.attribute_count += 1;
- }
- }
-
- self.current_stream_token = Some(token);
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
- Err(error) => {
- self.current_stream_token = None;
- if self.expecting_more_input && is_incomplete_xml_stream_error(&error) {
- self.paused_at_incomplete_input = true;
- break;
- }
-
- if summary.token_count == 0 {
- self.last_error = Some(error);
- self.exhausted = true;
- } else {
- self.pending_stream_error = Some(error);
- }
- break;
- }
- }
- }
-
- return if summary.token_count == 0 {
- None
- } else {
- Some(xml_token_stream_summary(&summary))
- };
- }
-
- while summary.tag_count < tag_limit {
- let next = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before tag count iteration");
- if next >= document.tokens.len() {
- self.current = None;
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
-
- self.current = Some(next);
- let token = &document.tokens[next];
- summary.token_count += 1;
-
- if token.token_type != "#tag" || token.closing {
- continue;
- }
-
- summary.tag_count += 1;
- if token.attributes.contains_key(&attribute_name) {
- summary.attribute_count += 1;
- }
- }
-
- if summary.token_count == 0 {
- None
- } else {
- Some(xml_token_stream_summary(&summary))
- }
- }
-
- pub fn next_tag_count_compact_batch(
- &mut self,
- max_tags: i64,
- attribute_name: String,
- ) -> Option {
- self.next_tag_count_batch(max_tags, attribute_name)
- }
-
- pub fn next_matching_tag_compact_summary_batch(
- &mut self,
- max_tags: i64,
- tag_namespace: String,
- tag_local_name: String,
- attribute_name: String,
- ) -> Option {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- 64
- };
- let mut summaries = String::new();
- let mut tag_count = 0;
- let mut token_delta = 0;
-
- if self.exhausted {
- return None;
- }
-
- if self.document.is_none() {
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return None;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before matching tag iteration");
- while tag_count < limit {
- match parse_next_xml_stream_token(&self.source, state) {
- Ok(Some(token)) => {
- self.paused_at_incomplete_input = false;
- token_delta += 1;
-
- if !xml_token_matches_tag_name(&token, &tag_namespace, &tag_local_name) {
- self.current_stream_token = Some(token);
- continue;
- }
-
- if tag_count > 0 {
- summaries.push('\x1e');
- }
- summaries.push_str(&xml_tag_compact_summary(
- &token,
- token_delta,
- &attribute_name,
- ));
- self.current_stream_token = Some(token);
- tag_count += 1;
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
- Err(error) => {
- self.current_stream_token = None;
- if self.expecting_more_input && is_incomplete_xml_stream_error(&error) {
- self.paused_at_incomplete_input = true;
- break;
- }
-
- if summaries.is_empty() {
- self.last_error = Some(error);
- self.exhausted = true;
- } else {
- self.pending_stream_error = Some(error);
- }
- break;
- }
- }
- }
-
- return if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- };
- }
-
- while tag_count < limit {
- let next = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before matching tag iteration");
- if next >= document.tokens.len() {
- self.current = None;
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
-
- self.current = Some(next);
- token_delta += 1;
-
- let token = &document.tokens[next];
- if !xml_token_matches_tag_name(token, &tag_namespace, &tag_local_name) {
- continue;
- }
-
- if tag_count > 0 {
- summaries.push('\x1e');
- }
- summaries.push_str(&xml_tag_compact_summary(
- token,
- token_delta,
- &attribute_name,
- ));
- tag_count += 1;
- }
-
- if summaries.is_empty() {
- None
- } else {
- Some(summaries)
- }
- }
-
- pub fn next_matching_tag_summary_batch(
- &mut self,
- max_tags: i64,
- tag_namespace: String,
- tag_local_name: String,
- attribute_name: String,
- ) -> Vec> {
- let limit = if max_tags > 0 && !tag_local_name.is_empty() {
- max_tags.min(256) as usize
- } else {
- return Vec::new();
- };
- let mut rows = Vec::new();
-
- while rows.len() < limit && self.next_token() {
- let Some(token) = self.current_token() else {
- continue;
- };
-
- if !xml_token_matches_tag_name(token, &tag_namespace, &tag_local_name) {
- continue;
- }
-
- rows.push(xml_tag_public_summary_row(token, &attribute_name));
- }
-
- rows
- }
-
- pub fn next_matching_tag_count_batch(
- &mut self,
- max_tags: i64,
- tag_namespace: String,
- tag_local_name: String,
- attribute_name: String,
- ) -> Option {
- let limit = if max_tags > 0 {
- max_tags.min(256) as usize
- } else {
- 64
- };
- let tag_limit = limit as i64;
- let mut summary = XmlTokenStreamSummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- };
-
- if self.exhausted {
- return None;
- }
-
- if self.document.is_none() {
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return None;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before matching tag count iteration");
- while summary.tag_count < tag_limit {
- match parse_next_xml_stream_token(&self.source, state) {
- Ok(Some(token)) => {
- self.paused_at_incomplete_input = false;
- summary.token_count += 1;
-
- if xml_token_matches_tag_name(&token, &tag_namespace, &tag_local_name) {
- summary.tag_count += 1;
- if token.attributes.contains_key(&attribute_name) {
- summary.attribute_count += 1;
- }
- }
-
- self.current_stream_token = Some(token);
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
- Err(error) => {
- self.current_stream_token = None;
- if self.expecting_more_input && is_incomplete_xml_stream_error(&error) {
- self.paused_at_incomplete_input = true;
- break;
- }
-
- if summary.token_count == 0 {
- self.last_error = Some(error);
- self.exhausted = true;
- } else {
- self.pending_stream_error = Some(error);
- }
- break;
- }
- }
- }
-
- return if summary.token_count == 0 {
- None
- } else {
- Some(xml_token_stream_summary(&summary))
- };
- }
-
- while summary.tag_count < tag_limit {
- let next = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before matching tag count iteration");
- if next >= document.tokens.len() {
- self.current = None;
- self.current_stream_token = None;
- self.exhausted = true;
- break;
- }
-
- self.current = Some(next);
- let token = &document.tokens[next];
- summary.token_count += 1;
-
- if !xml_token_matches_tag_name(token, &tag_namespace, &tag_local_name) {
- continue;
- }
-
- summary.tag_count += 1;
- if token.attributes.contains_key(&attribute_name) {
- summary.attribute_count += 1;
- }
- }
-
- if summary.token_count == 0 {
- None
- } else {
- Some(xml_token_stream_summary(&summary))
- }
- }
-
- pub fn next_matching_tag_count_compact_batch(
- &mut self,
- max_tags: i64,
- tag_namespace: String,
- tag_local_name: String,
- attribute_name: String,
- ) -> Option {
- self.next_matching_tag_count_batch(max_tags, tag_namespace, tag_local_name, attribute_name)
- }
-
- pub fn summarize_matching_tag_stream(
- &mut self,
- tag_namespace: String,
- tag_local_name: String,
- attribute_name: String,
- ) -> String {
- if self.exhausted || tag_local_name.is_empty() {
- return "0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlTokenStreamSummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_matching_tag_stream_token(
- token,
- &tag_namespace,
- &tag_local_name,
- &attribute_name,
- &mut summary,
- );
- }
- }
-
- return xml_token_stream_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_matching_tag_stream(
- &self.source,
- &tag_namespace,
- &tag_local_name,
- &attribute_name,
- );
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_token_stream_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing matching tags");
- let summary = summarize_xml_matching_tag_stream(
- &document.tokens[start..],
- &tag_namespace,
- &tag_local_name,
- &attribute_name,
- );
-
- self.current = None;
- self.exhausted = true;
-
- xml_token_stream_summary(&summary)
- }
-
- pub fn summarize_matching_tag_attributes_stream(
- &mut self,
- tag_namespace: String,
- tag_local_name: String,
- attribute_names: String,
- ) -> String {
- if self.exhausted || tag_local_name.is_empty() {
- return "0\x1f0\x1f0".to_string();
- }
-
- let attribute_names = parse_compact_attribute_names(&attribute_names);
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlTokenStreamSummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_matching_tag_attributes_stream_token(
- token,
- &tag_namespace,
- &tag_local_name,
- &attribute_names,
- &mut summary,
- );
- }
- }
-
- return xml_token_stream_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_matching_tag_attributes_stream(
- &self.source,
- &tag_namespace,
- &tag_local_name,
- &attribute_names,
- );
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_token_stream_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing matching tag attributes");
- let summary = summarize_xml_matching_tag_attributes_stream(
- &document.tokens[start..],
- &tag_namespace,
- &tag_local_name,
- &attribute_names,
- );
-
- self.current = None;
- self.exhausted = true;
-
- xml_token_stream_summary(&summary)
- }
-
- #[php(optional = query_or_ns)]
- pub fn next_tag(
- &mut self,
- query_or_ns: Option<&Zval>,
- null_or_local_name: Option,
- ) -> bool {
- let Some(query) = xml_parse_next_tag_query(query_or_ns, null_or_local_name) else {
- return false;
- };
-
- let mut match_offset = query.match_offset.max(1);
-
- while self.next_token() {
- if self.get_token_type().as_deref() != Some("#tag") || self.is_tag_closer() {
- continue;
- }
-
- if let Some(breadcrumbs) = query.breadcrumbs.as_ref() {
- let Some(current_breadcrumbs) = self.current_token_breadcrumbs() else {
- continue;
- };
-
- if !xml_namespaced_breadcrumbs_match(¤t_breadcrumbs, breadcrumbs) {
- continue;
- }
- }
-
- match_offset -= 1;
- if match_offset == 0 {
- return true;
- }
- }
-
- false
- }
-
- pub fn get_token_name(&self) -> Option {
- let token = self.current_token()?;
-
- Some(token.local_name.clone())
- }
-
- pub fn get_token_type(&self) -> Option {
- let token = self.current_token()?;
-
- Some(token.token_type.clone())
- }
-
- pub fn is_tag_closer(&self) -> bool {
- self.current_token()
- .map(|token| token.closing)
- .unwrap_or(false)
- }
-
- pub fn is_empty_element(&self) -> bool {
- self.current_token()
- .map(|token| token.empty_element)
- .unwrap_or(false)
- }
-
- pub fn expects_closer(&self) -> bool {
- self.current_token()
- .map(|token| token.token_type == "#tag" && !token.closing && !token.empty_element)
- .unwrap_or(false)
- }
-
- pub fn is_tag_opener(&self) -> bool {
- self.expects_closer()
- }
-
- pub fn get_token_byte_offset_in_the_input_stream(&self) -> i64 {
- self.current_token()
- .map(|token| token.start_offset as i64)
- .unwrap_or(0)
- }
-
- pub fn get_attribute(
- &self,
- namespace_or_name: String,
- local_name: Option,
- ) -> Option {
- let token = self.current_token()?;
-
- if token.attributes.is_empty() {
- return None;
- }
-
- if token.token_type != "#tag" && token.token_type != "#xml-declaration" {
- return None;
- }
-
- let name = match local_name {
- Some(local_name) if namespace_or_name.is_empty() => local_name,
- Some(local_name) => format!("{{{}}}{}", namespace_or_name, local_name),
- None => namespace_or_name,
- };
-
- token.attributes.get(&name).cloned()
- }
-
- pub fn get_attribute_names_with_prefix(
- &self,
- full_namespace_prefix: Option,
- local_name_prefix: String,
- ) -> Option>> {
- self.current_token()
- .filter(|token| token.token_type == "#tag" && !token.closing)
- .map(|token| {
- let mut matches = Vec::new();
- for attribute_name in &token.attribute_order {
- let (namespace, local_name) = split_resolved_attribute_name(attribute_name);
- if !local_name.starts_with(&local_name_prefix) {
- continue;
- }
-
- match &full_namespace_prefix {
- Some(prefix) if namespace.starts_with(prefix) => {
- matches.push(vec![namespace.to_string(), local_name.to_string()])
- }
- None if namespace.is_empty() => {
- matches.push(vec![namespace.to_string(), local_name.to_string()])
- }
- _ => {}
- }
- }
- matches
- })
- }
-
- pub fn summarize_attribute_names_with_prefix(
- &mut self,
- full_namespace_prefix: Option,
- local_name_prefix: String,
- ) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlAttributePrefixSummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_token_attribute_names_with_prefix(
- token,
- full_namespace_prefix.as_deref(),
- &local_name_prefix,
- &mut summary,
- );
- }
- }
-
- return format!(
- "{}\x1f{}\x1f{}",
- summary.token_count, summary.tag_count, summary.attribute_count
- );
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_attribute_names_with_prefix(
- &self.source,
- full_namespace_prefix.as_deref(),
- &local_name_prefix,
- );
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return format!(
- "{}\x1f{}\x1f{}",
- scan.summary.token_count, scan.summary.tag_count, scan.summary.attribute_count
- );
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing remaining tokens");
- let summary = summarize_xml_attribute_names_with_prefix(
- &document.tokens[start..],
- full_namespace_prefix.as_deref(),
- &local_name_prefix,
- );
-
- self.current = None;
- self.exhausted = true;
-
- format!(
- "{}\x1f{}\x1f{}",
- summary.token_count, summary.tag_count, summary.attribute_count
- )
- }
-
- pub fn remove_attributes_with_prefix_from_document(
- &mut self,
- full_namespace_prefix: Option,
- local_name_prefix: String,
- ) -> String {
- if self.exhausted {
- return format!("0\x1f0\x1f{}", self.source);
- }
-
- if self.current.is_some() || self.document.is_some() {
- self.ensure_document();
- self.current = None;
- self.exhausted = true;
-
- return format!("0\x1f0\x1f{}", self.source);
- }
-
- let scan = remove_xml_source_attributes_with_prefix(
- &self.source,
- full_namespace_prefix.as_deref(),
- &local_name_prefix,
- );
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- format!(
- "{}\x1f{}\x1f{}",
- scan.summary.tag_count, scan.summary.attribute_count, scan.xml
- )
- }
-
- pub fn summarize_token_stream(&mut self, attribute_name: String) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlTokenStreamSummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_token_stream_token(token, &attribute_name, &mut summary);
- }
- }
-
- return format!(
- "{}\x1f{}\x1f{}",
- summary.token_count, summary.tag_count, summary.attribute_count
- );
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_token_stream(&self.source, &attribute_name);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return format!(
- "{}\x1f{}\x1f{}",
- scan.summary.token_count, scan.summary.tag_count, scan.summary.attribute_count
- );
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing remaining tokens");
- let summary = summarize_xml_token_stream(&document.tokens[start..], &attribute_name);
-
- self.current = None;
- self.exhausted = true;
-
- format!(
- "{}\x1f{}\x1f{}",
- summary.token_count, summary.tag_count, summary.attribute_count
- )
- }
-
- pub fn summarize_document_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlDocumentInventorySummary {
- token_count: 0,
- tag_count: 0,
- closing_tag_count: 0,
- text_token_count: 0,
- comment_count: 0,
- cdata_count: 0,
- max_depth: 0,
- empty_element_count: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_document_inventory_token(token, &mut summary);
- }
- }
-
- return xml_document_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_document_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_document_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing document inventory");
- let summary = summarize_xml_document_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_document_inventory_summary(&summary)
- }
-
- pub fn summarize_element_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlElementInventorySummary {
- token_count: 0,
- tag_count: 0,
- closing_tag_count: 0,
- unique_tag_name_count: 0,
- duplicate_tag_name_count: 0,
- namespaced_tag_count: 0,
- empty_element_count: 0,
- };
- let mut tag_names = HashSet::new();
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_element_inventory_token(token, &mut summary, &mut tag_names);
- }
- }
- summary.unique_tag_name_count = tag_names.len() as i64;
-
- return xml_element_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_element_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_element_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing element inventory");
- let summary = summarize_xml_element_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_element_inventory_summary(&summary)
- }
-
- pub fn summarize_depth_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlDepthInventorySummary {
- token_count: 0,
- tag_count: 0,
- closing_tag_count: 0,
- empty_element_count: 0,
- root_level_tag_count: 0,
- nested_tag_count: 0,
- total_tag_depth: 0,
- max_depth: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_depth_inventory_token(token, &mut summary);
- }
- }
-
- return xml_depth_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_depth_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_depth_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing depth inventory");
- let summary = summarize_xml_depth_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_depth_inventory_summary(&summary)
- }
-
- pub fn summarize_attribute_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlAttributeInventorySummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- namespaced_attribute_count: 0,
- tags_with_attributes_count: 0,
- max_attribute_count: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_attribute_inventory_token(token, &mut summary);
- }
- }
-
- return xml_attribute_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_attribute_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_attribute_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing attribute inventory");
- let summary = summarize_xml_attribute_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_attribute_inventory_summary(&summary)
- }
-
- pub fn summarize_attribute_inventory_array(&mut self) -> Vec<(String, Zval)> {
- if self.exhausted {
- return xml_attribute_inventory_public_summary_row(&XmlAttributeInventorySummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- namespaced_attribute_count: 0,
- tags_with_attributes_count: 0,
- max_attribute_count: 0,
- });
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlAttributeInventorySummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- namespaced_attribute_count: 0,
- tags_with_attributes_count: 0,
- max_attribute_count: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_attribute_inventory_token(token, &mut summary);
- }
- }
-
- return xml_attribute_inventory_public_summary_row(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_attribute_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_attribute_inventory_public_summary_row(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing attribute inventory");
- let summary = summarize_xml_attribute_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_attribute_inventory_public_summary_row(&summary)
- }
-
- pub fn summarize_id_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlIdInventorySummary {
- token_count: 0,
- tag_count: 0,
- id_attribute_count: 0,
- unique_id_count: 0,
- duplicate_id_count: 0,
- id_value_bytes: 0,
- };
- let mut seen_ids = HashSet::new();
- let mut duplicate_ids = HashSet::new();
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_id_inventory_token(
- token,
- &mut summary,
- &mut seen_ids,
- &mut duplicate_ids,
- );
- }
- }
-
- return xml_id_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_id_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_id_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing ID inventory");
- let summary = summarize_xml_id_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_id_inventory_summary(&summary)
- }
-
- pub fn summarize_id_inventory_array(&mut self) -> Vec<(String, Zval)> {
- if self.exhausted {
- return xml_id_inventory_public_summary_row(&XmlIdInventorySummary {
- token_count: 0,
- tag_count: 0,
- id_attribute_count: 0,
- unique_id_count: 0,
- duplicate_id_count: 0,
- id_value_bytes: 0,
- });
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlIdInventorySummary {
- token_count: 0,
- tag_count: 0,
- id_attribute_count: 0,
- unique_id_count: 0,
- duplicate_id_count: 0,
- id_value_bytes: 0,
- };
- let mut seen_ids = HashSet::new();
- let mut duplicate_ids = HashSet::new();
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_id_inventory_token(
- token,
- &mut summary,
- &mut seen_ids,
- &mut duplicate_ids,
- );
- }
- }
-
- return xml_id_inventory_public_summary_row(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_id_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_id_inventory_public_summary_row(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing ID inventory");
- let summary = summarize_xml_id_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_id_inventory_public_summary_row(&summary)
- }
-
- pub fn summarize_namespace_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlNamespaceInventorySummary {
- token_count: 0,
- tag_count: 0,
- namespaced_tag_count: 0,
- attribute_count: 0,
- namespaced_attribute_count: 0,
- unique_namespace_count: 0,
- };
- let mut namespaces = HashSet::new();
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_namespace_inventory_token(token, &mut summary, &mut namespaces);
- }
- }
- summary.unique_namespace_count = namespaces.len() as i64;
-
- return xml_namespace_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_namespace_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_namespace_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing namespace inventory");
- let summary = summarize_xml_namespace_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_namespace_inventory_summary(&summary)
- }
-
- pub fn summarize_text_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlTextInventorySummary {
- token_count: 0,
- text_token_count: 0,
- cdata_count: 0,
- non_empty_text_count: 0,
- whitespace_text_count: 0,
- total_text_bytes: 0,
- max_text_bytes: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_text_inventory_token(token, &mut summary);
- }
- }
-
- return xml_text_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_text_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_text_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing text inventory");
- let summary = summarize_xml_text_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_text_inventory_summary(&summary)
- }
-
- pub fn summarize_processing_instruction_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlProcessingInstructionInventorySummary {
- token_count: 0,
- processing_instruction_count: 0,
- xml_declaration_count: 0,
- non_empty_instruction_count: 0,
- total_instruction_bytes: 0,
- max_instruction_bytes: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_processing_instruction_inventory_token(token, &mut summary);
- }
- }
-
- return xml_processing_instruction_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_processing_instruction_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_processing_instruction_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self.document.as_ref().expect(
- "XML document should be parsed before summarizing processing instruction inventory",
- );
- let summary = summarize_xml_processing_instruction_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_processing_instruction_inventory_summary(&summary)
- }
-
- pub fn summarize_comment_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlCommentInventorySummary {
- token_count: 0,
- comment_count: 0,
- non_empty_comment_count: 0,
- empty_comment_count: 0,
- total_comment_bytes: 0,
- max_comment_bytes: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_comment_inventory_token(token, &mut summary);
- }
- }
-
- return xml_comment_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_comment_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_comment_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing comment inventory");
- let summary = summarize_xml_comment_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_comment_inventory_summary(&summary)
- }
-
- pub fn summarize_payload_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlPayloadInventorySummary {
- token_count: 0,
- text_token_count: 0,
- cdata_count: 0,
- comment_count: 0,
- processing_instruction_count: 0,
- total_payload_bytes: 0,
- max_payload_bytes: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_payload_inventory_token(token, &mut summary);
- }
- }
-
- return xml_payload_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_payload_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_payload_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing payload inventory");
- let summary = summarize_xml_payload_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_payload_inventory_summary(&summary)
- }
-
- pub fn summarize_content_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlContentInventorySummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- text_token_count: 0,
- cdata_count: 0,
- comment_count: 0,
- processing_instruction_count: 0,
- total_attribute_value_bytes: 0,
- max_attribute_value_bytes: 0,
- total_payload_bytes: 0,
- max_payload_bytes: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_content_inventory_token(token, &mut summary);
- }
- }
-
- return xml_content_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_content_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_content_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing content inventory");
- let summary = summarize_xml_content_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_content_inventory_summary(&summary)
- }
-
- pub fn summarize_leaf_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlLeafInventorySummary {
- token_count: 0,
- tag_count: 0,
- closing_tag_count: 0,
- empty_element_count: 0,
- leaf_element_count: 0,
- branch_element_count: 0,
- max_child_element_count: 0,
- };
- let mut open_child_counts = Vec::new();
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_leaf_inventory_token(token, &mut summary, &mut open_child_counts);
- }
- }
-
- return xml_leaf_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_leaf_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_leaf_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing leaf inventory");
- let summary = summarize_xml_leaf_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_leaf_inventory_summary(&summary)
- }
-
- pub fn summarize_structural_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0"
- .to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = empty_xml_structural_inventory_summary();
- let mut tag_names = HashSet::new();
- let mut open_child_counts = Vec::new();
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_structural_inventory_token(
- token,
- &mut summary,
- &mut tag_names,
- &mut open_child_counts,
- );
- }
- }
- summary.unique_tag_name_count = tag_names.len() as i64;
-
- return xml_structural_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_structural_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_structural_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing structural inventory");
- let summary = summarize_xml_structural_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_structural_inventory_summary(&summary)
- }
-
- pub fn summarize_import_inventory(&mut self) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0\x1f0"
- .to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = empty_xml_import_inventory_summary();
- let mut tag_names = HashSet::new();
- let mut open_child_counts = Vec::new();
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_import_inventory_token(
- token,
- &mut summary,
- &mut tag_names,
- &mut open_child_counts,
- );
- }
- }
- summary.structural.unique_tag_name_count = tag_names.len() as i64;
-
- return xml_import_inventory_summary(&summary);
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_import_inventory(&self.source);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return xml_import_inventory_summary(&scan.summary);
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing import inventory");
- let summary = summarize_xml_import_inventory(&document.tokens[start..]);
-
- self.current = None;
- self.exhausted = true;
-
- xml_import_inventory_summary(&summary)
- }
-
- pub fn summarize_tag_stream(&mut self, attribute_name: String) -> String {
- if self.exhausted {
- return "0\x1f0\x1f0".to_string();
- }
-
- if self.document.is_none() && self.stream.is_some() {
- let mut summary = XmlTokenStreamSummary {
- token_count: 0,
- tag_count: 0,
- attribute_count: 0,
- };
-
- while self.next_stream_token() {
- if let Some(token) = self.current_stream_token.as_ref() {
- summarize_xml_tag_stream_token(token, &attribute_name, &mut summary);
- }
- }
-
- return format!(
- "{}\x1f{}\x1f{}",
- summary.token_count, summary.tag_count, summary.attribute_count
- );
- }
-
- if self.current.is_none() && self.document.is_none() {
- let scan = summarize_xml_source_tag_stream(&self.source, &attribute_name);
-
- self.last_error = scan.error;
- self.current = None;
- self.exhausted = true;
-
- return format!(
- "{}\x1f{}\x1f{}",
- scan.summary.token_count, scan.summary.tag_count, scan.summary.attribute_count
- );
- }
-
- self.ensure_document();
-
- let start = self.current.map_or(0, |index| index + 1);
- let document = self
- .document
- .as_ref()
- .expect("XML document should be parsed before summarizing remaining tags");
- let summary = summarize_xml_tag_stream(&document.tokens[start..], &attribute_name);
-
- self.current = None;
- self.exhausted = true;
-
- format!(
- "{}\x1f{}\x1f{}",
- summary.token_count, summary.tag_count, summary.attribute_count
- )
- }
-
- pub fn get_tag_local_name(&self) -> Option {
- let token = self.current_token()?;
-
- if token.token_type == "#tag" {
- Some(token.local_name.clone())
- } else {
- None
- }
- }
-
- pub fn get_tag_namespace(&self) -> Option {
- self.current_token().and_then(|token| {
- if token.token_type == "#tag" {
- Some(token.namespace.clone().unwrap_or_default())
- } else {
- None
- }
- })
- }
-
- pub fn get_tag_namespace_and_local_name(&self) -> Option {
- self.current_token().and_then(|token| {
- if token.token_type == "#tag" {
- Some(match &token.namespace {
- Some(namespace) if !namespace.is_empty() => {
- format!("{{{}}}{}", namespace, token.local_name)
- }
- _ => token.local_name.clone(),
- })
- } else {
- None
- }
- })
- }
-
- pub fn get_modifiable_text(&self) -> String {
- self.current_token()
- .map(|token| {
- if token.token_type == "#tag" && !token.closing && !token.empty_element {
- return xml_tag_modifiable_text(
- self.source.as_bytes(),
- xml_tag_content_start(self.source.as_bytes(), token.start_offset),
- &token.name,
- );
- }
-
- if token.text.is_empty() && token.text_start != XML_TEXT_RANGE_NONE {
- let bytes = self.source.as_bytes();
- return if token.token_type == "#text" {
- decode_xml_text_bytes(&bytes[token.text_start..token.text_end])
- } else {
- normalize_xml_text(&String::from_utf8_lossy(
- &bytes[token.text_start..token.text_end],
- ))
- };
- }
-
- token.text.clone()
- })
- .unwrap_or_default()
- }
-
- pub fn get_doctype_name(&self) -> Option {
- self.current_token().and_then(|token| {
- if token.token_type != "#doctype" {
- return None;
- }
-
- parse_doctype_parts(self.source.as_bytes(), token.start_offset).0
- })
- }
-
- pub fn get_system_literal(&self) -> Option {
- self.current_token().and_then(|token| {
- if token.token_type != "#doctype" {
- return None;
- }
-
- parse_doctype_parts(self.source.as_bytes(), token.start_offset).1
- })
- }
-
- pub fn get_pubid_literal(&self) -> Option {
- self.current_token().and_then(|token| {
- if token.token_type != "#doctype" {
- return None;
- }
-
- parse_doctype_parts(self.source.as_bytes(), token.start_offset).2
- })
- }
-
- pub fn set_modifiable_text(&mut self, new_value: String) -> bool {
- let token = match self.current_token() {
- Some(token)
- if (token.token_type == "#text"
- || token.token_type == "#comment"
- || token.token_type == "#cdata-section")
- && token.text_start != XML_TEXT_RANGE_NONE =>
- {
- token.clone()
- }
- _ => return false,
- };
- if self.document.is_some() {
- return false;
- }
-
- let replacement = if token.token_type == "#cdata-section" {
- new_value.replace("]]>", "]]>")
- } else {
- escape_xml_text_value(&new_value)
- };
- let replacement_len = replacement.len();
- self.apply_source_edit(token.text_start, token.text_end, &replacement);
- if let Some(current_token) = self.current_stream_token.as_mut() {
- current_token.text = new_value;
- current_token.text_end = current_token.text_start + replacement_len;
- }
-
- true
- }
-
- pub fn set_attribute(
- &mut self,
- xml_namespace: String,
- local_name: String,
- value: String,
- ) -> bool {
- if xml_namespace == "xmlns" || !is_xml_unprefixed_name(&local_name) {
- return false;
- }
-
- let token = match self.current_token() {
- Some(token) if token.token_type == "#tag" && !token.closing => token.clone(),
- _ => return false,
- };
- if self.document.is_some() {
- return false;
- }
-
- let name_end = match xml_tag_name_end_at(self.source.as_bytes(), token.start_offset) {
- Some(name_end) => name_end,
- None => return false,
- };
- let tag_attributes =
- match parse_xml_source_tag_attributes(self.source.as_bytes(), name_end, false) {
- Ok(tag_attributes) => tag_attributes,
- Err(_) => return false,
- };
-
- let attribute_name = match self.current_source_attribute_name(
- &xml_namespace,
- &local_name,
- &tag_attributes,
- ) {
- Some(attribute_name) => attribute_name,
- None => return false,
- };
- let resolved_attribute_name = if xml_namespace.is_empty() {
- local_name.clone()
- } else {
- format!("{{{}}}{}", xml_namespace, local_name)
- };
- let serialized_attribute = format!(
- "{}=\"{}\"",
- attribute_name,
- escape_xml_attribute_value(&value)
- );
- for attribute in &tag_attributes.attributes {
- if self.source_attribute_resolves_to(
- &attribute.name,
- &xml_namespace,
- &local_name,
- &tag_attributes,
- ) {
- self.apply_source_edit(attribute.start, attribute.end, &serialized_attribute);
- self.update_current_attribute(resolved_attribute_name, Some(value));
- return true;
- }
- }
-
- self.apply_source_edit(name_end, name_end, &format!(" {}", serialized_attribute));
- self.update_current_attribute(resolved_attribute_name, Some(value));
- true
- }
-
- pub fn remove_attribute(&mut self, xml_namespace: String, local_name: String) -> bool {
- if xml_namespace == "xmlns" || !is_xml_unprefixed_name(&local_name) {
- return false;
- }
-
- let token = match self.current_token() {
- Some(token) if token.token_type == "#tag" && !token.closing => token.clone(),
- _ => return false,
- };
- if self.document.is_some() {
- return false;
- }
-
- let name_end = match xml_tag_name_end_at(self.source.as_bytes(), token.start_offset) {
- Some(name_end) => name_end,
- None => return false,
- };
- let tag_attributes =
- match parse_xml_source_tag_attributes(self.source.as_bytes(), name_end, false) {
- Ok(tag_attributes) => tag_attributes,
- Err(_) => return false,
- };
-
- let resolved_attribute_name = if xml_namespace.is_empty() {
- local_name
- } else {
- format!("{{{}}}{}", xml_namespace, local_name)
- };
- for attribute in &tag_attributes.attributes {
- if self.source_attribute_resolves_to(
- &attribute.name,
- &xml_namespace,
- split_resolved_attribute_name(&resolved_attribute_name).1,
- &tag_attributes,
- ) {
- self.apply_source_edit(attribute.start, attribute.end, "");
- self.update_current_attribute(resolved_attribute_name, None);
- return true;
- }
- }
-
- false
- }
-
- pub fn get_updated_xml(&self) -> String {
- self.source.clone()
- }
-
- #[php(name = "__toString")]
- pub fn __to_string(&self) -> String {
- self.get_updated_xml()
- }
-
- pub fn set_bookmark(&mut self, name: String) -> bool {
- if self.current_token().is_none() {
- return false;
- }
-
- self.bookmarks.insert(
- name,
- XmlBookmark {
- current: self.current,
- stream: self.stream.clone(),
- stream_reentrancy_base_state: self.stream_reentrancy_base_state.clone(),
- current_stream_token: self.current_stream_token.clone(),
- current_stream_token_start_state: self.current_stream_token_start_state.clone(),
- exhausted: self.exhausted,
- expecting_more_input: self.expecting_more_input,
- paused_at_incomplete_input: self.paused_at_incomplete_input,
- last_error: self.last_error.clone(),
- pending_stream_error: self.pending_stream_error.clone(),
- },
- );
-
- true
- }
-
- pub fn release_bookmark(&mut self, name: String) -> bool {
- self.bookmarks.remove(&name).is_some()
- }
-
- pub fn has_bookmark(&self, name: String) -> bool {
- self.bookmarks.contains_key(&name)
- }
-
- pub fn seek(&mut self, name: String) -> bool {
- let Some(bookmark) = self.bookmarks.get(&name).cloned() else {
- return false;
- };
-
- self.current = bookmark.current;
- self.stream = bookmark.stream;
- self.stream_reentrancy_base_state = bookmark.stream_reentrancy_base_state;
- self.current_stream_token = bookmark.current_stream_token;
- self.current_stream_token_start_state = bookmark.current_stream_token_start_state;
- self.exhausted = bookmark.exhausted;
- self.expecting_more_input = bookmark.expecting_more_input;
- self.paused_at_incomplete_input = bookmark.paused_at_incomplete_input;
- self.last_error = bookmark.last_error;
- self.pending_stream_error = bookmark.pending_stream_error;
-
- true
- }
-
- pub fn get_breadcrumbs(&self) -> Vec> {
- self.current_token_breadcrumbs()
- .map(|breadcrumbs| {
- breadcrumbs
- .into_iter()
- .map(|(namespace, local_name)| vec![namespace, local_name])
- .collect()
- })
- .unwrap_or_default()
- }
-
- pub fn matches_breadcrumbs(&self, breadcrumbs: Vec) -> bool {
- if breadcrumbs.is_empty() {
- return true;
- }
-
- match self.current_token() {
- Some(token) if token.token_type == "#tag" => {}
- _ => return false,
- }
-
- let token_breadcrumbs = match self.current_token_breadcrumbs() {
- Some(token_breadcrumbs) => token_breadcrumbs,
- None => return false,
- };
-
- if breadcrumbs.len() > token_breadcrumbs.len() {
- return false;
- }
-
- let offset = token_breadcrumbs.len() - breadcrumbs.len();
- for (index, breadcrumb) in breadcrumbs.iter().enumerate() {
- let (_, local_name) = &token_breadcrumbs[offset + index];
- if breadcrumb != "*" && breadcrumb != local_name {
- return false;
- }
- }
-
- true
- }
-
- pub fn get_current_depth(&self) -> i64 {
- self.current_token()
- .map(|token| token.depth as i64)
- .unwrap_or(0)
- }
-
- pub fn is_finished(&self) -> bool {
- self.exhausted
- && !self.expecting_more_input
- && self.last_error.is_none()
- && self
- .document
- .as_ref()
- .map(|document| document.error.is_none())
- .unwrap_or(true)
- }
-
- pub fn input_finished(&mut self) {
- self.expecting_more_input = false;
- if self.paused_at_incomplete_input {
- self.paused_at_incomplete_input = false;
- self.exhausted = false;
- if let Some(stream) = self.stream.as_mut() {
- stream.finished = false;
- }
- }
- }
-
- pub fn append_bytes(&mut self, next_chunk: String) -> bool {
- if !self.expecting_more_input {
- return false;
- }
-
- self.source.push_str(&next_chunk);
- self.exhausted = false;
- self.paused_at_incomplete_input = false;
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.last_error = None;
- self.pending_stream_error = None;
- if let Some(stream) = self.stream.as_mut() {
- stream.finished = false;
- }
-
- true
- }
-
- pub fn is_expecting_more_input(&self) -> bool {
- self.expecting_more_input
- }
-
- pub fn is_paused_at_incomplete_input(&self) -> bool {
- self.paused_at_incomplete_input
- }
-
- pub fn get_last_error(&self) -> Option {
- if self.paused_at_incomplete_input {
- return None;
- }
-
- if self.exhausted && self.document.is_none() {
- return self.last_error.clone();
- }
-
- if self.stream.is_some() {
- return self.last_error.clone();
- }
-
- self.document
- .as_ref()
- .map(|document| document.error.clone())
- .unwrap_or_else(|| parse_xml_document(&self.source).error)
- }
-
- pub fn get_exception(&self) -> Option {
- self.get_last_error()
- }
-
- pub fn current_token_metadata(&self) -> Option {
- self.current_token().map(xml_token_metadata)
- }
-}
-
-#[cfg(feature = "php-extension")]
-impl NativeXmlProcessor {
- fn ensure_document(&mut self) {
- if self.document.is_none() {
- let document = parse_xml_document(&self.source);
- self.last_error = document.error.clone();
- self.document = Some(document);
- self.stream = None;
- self.stream_reentrancy_base_state = None;
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- }
- }
-
- fn next_cursor_stream_token(&mut self) -> bool {
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return false;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before token iteration");
- match parse_next_xml_stream_token_with_payloads(&self.source, state, false, false) {
- Ok(Some(token)) => {
- self.current_stream_token = Some(token);
- self.current_stream_token_start_state = None;
- self.paused_at_incomplete_input = false;
- true
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.exhausted = true;
- false
- }
- Err(error) if self.expecting_more_input && is_incomplete_xml_stream_error(&error) => {
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.paused_at_incomplete_input = true;
- false
- }
- Err(error) => {
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.last_error = Some(error);
- self.exhausted = true;
- false
- }
- }
- }
-
- fn next_stream_token(&mut self) -> bool {
- if let Some(error) = self.pending_stream_error.take() {
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.last_error = Some(error);
- self.exhausted = true;
- return false;
- }
-
- if self.stream.is_none() {
- self.stream = Some(XmlStreamState::new());
- }
-
- let state = self
- .stream
- .as_mut()
- .expect("XML stream should be initialized before token iteration");
- match parse_next_xml_stream_token(&self.source, state) {
- Ok(Some(token)) => {
- self.current_stream_token = Some(token);
- self.current_stream_token_start_state = None;
- self.paused_at_incomplete_input = false;
- true
- }
- Ok(None) => {
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.exhausted = true;
- false
- }
- Err(error) if self.expecting_more_input && is_incomplete_xml_stream_error(&error) => {
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.paused_at_incomplete_input = true;
- false
- }
- Err(error) => {
- self.current_stream_token = None;
- self.current_stream_token_start_state = None;
- self.last_error = Some(error);
- self.exhausted = true;
- false
- }
- }
- }
-
- fn current_token(&self) -> Option<&XmlToken> {
- if let Some(token) = self.current_stream_token.as_ref() {
- return Some(token);
- }
-
- self.current.and_then(|index| {
- self.document
- .as_ref()
- .and_then(|document| document.tokens.get(index))
- })
- }
-
- fn current_token_breadcrumbs(&self) -> Option