from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, TypeVar, overload

import httpx
from ollama import AsyncClient as OllamaAsyncClient
from ollama import Options
from pydantic import BaseModel

from browser_use.llm.base import BaseChatModel
from browser_use.llm.exceptions import ModelProviderError
from browser_use.llm.messages import BaseMessage
from browser_use.llm.ollama.serializer import OllamaMessageSerializer
from browser_use.llm.views import ChatInvokeCompletion

T = TypeVar('T', bound=BaseModel)

@dataclass
class ChatOllama(BaseChatModel):
	"""
	A wrapper around Ollama's chat model.
	"""

	model: str

	# # Model params
	# TODO (matic): Why is this commented out?
	# temperature: float | None = None

	# Client initialization parameters
	host: str | None = None
	timeout: float | httpx.Timeout | None = None
	client_params: dict[str, Any] | None = None
	ollama_options: Mapping[str, Any] | Options | None = None

	# Static
	@property
	def provider(self) -> str:
		return 'ollama'

	def _get_client_params(self) -> dict[str, Any]:
		"""Prepare client parameters dictionary."""
		return {
			'host': self.host,
			'timeout': self.timeout,
			'client_params': self.client_params,
		}

	def get_client(self) -> OllamaAsyncClient:
		"""
		Returns an OllamaAsyncClient client.
		"""
		return OllamaAsyncClient(host=self.host, timeout=self.timeout, **self.client_params or {})

	@property
	def name(self) -> str:
		return self.model

	@overload
	async def ainvoke(
		self, messages: list[BaseMessage], output_format: None = None, **kwargs: Any
	) -> ChatInvokeCompletion[str]: ...

	@overload
	async def ainvoke(self, messages: list[BaseMessage], output_format: type[T], **kwargs: Any) -> ChatInvokeCompletion[T]: ...

	async def ainvoke(
		self, messages: list[BaseMessage], output_format: type[T] | None = None, **kwargs: Any
	) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:
		ollama_messages = OllamaMessageSerializer.serialize_messages(messages)

		try:
			if output_format is None:
				response = await self.get_client().chat(
					model=self.model,
					messages=ollama_messages,
					options=self.ollama_options,
				)

				return ChatInvokeCompletion(completion=response.message.content or '', usage=None)
			else:
				schema = output_format.model_json_schema()

				response = await self.get_client().chat(
					model=self.model,
					messages=ollama_messages,
					format=schema,
					options=self.ollama_options,
				)

				completion = response.message.content or ''
				if output_format is not None:
					completion = output_format.model_validate_json(completion)

				return ChatInvokeCompletion(completion=completion, usage=None)

		except Exception as e:
			raise ModelProviderError(message=str(e), model=self.name) from e


/**
 * Upwork adapter utilities.
 *
 * Upwork is a Nuxt (Vue) SSR app behind Cloudflare. Every adapter runs
 * through the user's logged-in browser session (Strategy.COOKIE,
 * browser: true) because bare fetches hit a `__cf_bm` challenge and
 * because most surfaces only render data for an authenticated user.
 *
 * The list pages (search, best-matches feed) ship their full result
 * payload inside `window.__NUXT__.state` — we read straight from that
 * global instead of DOM-scraping rendered cards. Job detail uses the
 * Vuex store (`window.$nuxt.$store.state.jobDetails.*`). All helpers
 * below are pure (arg validation, decoders, URL builders, row mappers)
 * so they stay unit-testable without a browser.
 */

import {
    ArgumentError,
    CommandExecutionError,
} from '@jackwener/opencli/errors';

export const UPWORK_ORIGIN = 'https://www.upwork.com';

const CIPHERTEXT_PATTERN = /^~0[12]\d{15,21}$/;

const FEED_TABS = {
    'best-matches': { path: '/nx/find-work/best-matches', state: 'feedBestMatch' },
    'most-recent': { path: '/nx/find-work/most-recent', state: 'feedMostRecent' },
};

const SORT_VALUES = new Set(['recency', 'relevance', 'client_total_charge', 'client_total_reviews']);

export function unwrapBrowserResult(value) {
    if (value && typeof value === 'object' && !Array.isArray(value) && 'session' in value && 'data' in value) {
        return value.data;
    }
    return value;
}

export function isPlainObject(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function coerceInt(value) {
    if (value === undefined || value === null || value === '') return NaN;
    const n = typeof value === 'number' ? value : Number(value);
    return Number.isFinite(n) && Number.isInteger(n) ? n : NaN;
}

export function requireQuery(value, label = 'query') {
    const q = String(value ?? '').trim();
    if (!q) throw new ArgumentError(`upwork ${label} cannot be empty`);
    return q;
}

export function requirePositiveInt(value, defaultValue, label) {
    const raw = value ?? defaultValue;
    const n = coerceInt(raw);
    if (!Number.isInteger(n) || n <= 0) {
        throw new ArgumentError(`upwork ${label} must be a positive integer`);
    }
    return n;
}

export function requireBoundedInt(value, defaultValue, min, max, label) {
    const n = requirePositiveInt(value, defaultValue, label);
    if (n < min) throw new ArgumentError(`upwork ${label} must be >= ${min}`);
    if (n > max) throw new ArgumentError(`upwork ${label} must be <= ${max}`);
    return n;
}

/**
 * Upwork job ids are the ciphertext form starting with `~01` or `~02`
 * (the encoded uid surfaced everywhere in URLs and search results).
 * Accepts a bare ciphertext or a full `/jobs/~02…` URL.
 */
export function requireCiphertext(value) {
    let id = String(value ?? '').trim();
    if (!id) throw new ArgumentError('upwork job id is required');
    const urlMatch = id.match(/~0[12]\d+/);
    if (urlMatch) id = urlMatch[0];
    if (!CIPHERTEXT_PATTERN.test(id)) {
        throw new ArgumentError(`upwork job id "${value}" is not a valid ciphertext (expected ~01… or ~02… followed by digits)`);
    }
    return id;
}

export function requireFeedTab(value, defaultValue = 'best-matches') {
    const v = String(value ?? defaultValue).trim().toLowerCase();
    if (!FEED_TABS[v]) {
        throw new ArgumentError(`upwork tab must be one of ${Object.keys(FEED_TABS).join(' / ')}, got "${value}"`);
    }
    return v;
}

export function requireSort(value, defaultValue = 'recency') {
    const v = String(value ?? defaultValue).trim().toLowerCase();
    if (!SORT_VALUES.has(v)) {
        throw new ArgumentError(`upwork sort must be one of ${Array.from(SORT_VALUES).join(' / ')}, got "${value}"`);
    }
    return v;
}

/**
 * Build the Upwork search URL. Only forwards filters the user actually
 * supplied so the URL stays canonical and round-trippable.
 */
export function buildSearchUrl({ query, location, category, sort, page, perPage }) {
    const params = new URLSearchParams();
    params.set('q', query);
    if (location) params.set('location', location);
    if (category) params.set('category2_uid', category);
    if (sort && sort !== 'recency') params.set('sort', sort);
    if (perPage && perPage !== 10) params.set('per_page', String(perPage));
    if (page && page > 1) params.set('page', String(page));
    return `${UPWORK_ORIGIN}/nx/search/jobs/?${params.toString()}`;
}

export function buildFeedUrl(tab) {
    const t = FEED_TABS[tab];
    if (!t) throw new ArgumentError(`unknown feed tab "${tab}"`);
    return `${UPWORK_ORIGIN}${t.path}`;
}

export function feedStateKey(tab) {
    const t = FEED_TABS[tab];
    if (!t) throw new ArgumentError(`unknown feed tab "${tab}"`);
    return t.state;
}

export function buildJobUrl(ciphertext) {
    return `${UPWORK_ORIGIN}/jobs/${ciphertext}`;
}

export function isValidCiphertext(value) {
    return CIPHERTEXT_PATTERN.test(String(value ?? '').trim());
}

/**
 * Strip Upwork's `<span class="highlight">…</span>` markup that wraps
 * matched query terms in search results, then collapse whitespace.
 * Empty / null returns ''.
 */
export function stripHighlight(text) {
    if (text == null) return '';
    return String(text)
        .replace(/<span class="highlight">/g, '')
        .replace(/<\/span>/g, '')
        .replace(/\s+/g, ' ')
        .trim();
}

/**
 * Decode `tierText` codes into stable lowercase labels.
 *   - search rows use the i18n-keyed form: `jsn_Entry_205` / `_Intermediate_206` / `_Expert_207`
 *   - feed rows already pass through the rendered label: `Entry level` / `Intermediate` / `Expert`
 *   - detail uses a numeric `contractorTier`: 1 / 2 / 3
 * Returns 'entry' | 'intermediate' | 'expert' | '' (unknown).
 */
export function decodeExperienceLevel(value) {
    if (value == null || value === '') return '';
    if (typeof value === 'number') {
        if (value === 1) return 'entry';
        if (value === 2) return 'intermediate';
        if (value === 3) return 'expert';
        return '';
    }
    const v = String(value).toLowerCase();
    if (v.includes('entry')) return 'entry';
    if (v.includes('intermediate')) return 'intermediate';
    if (v.includes('expert')) return 'expert';
    return '';
}

/**
 * Decode the `engagement` workload code. Search rows ship it as
 * `usnuxt_Engagement_421.fullTime` / `.partTime`; detail surfaces it
 * pre-rendered as `More than 30 hrs/week`. Returns 'full-time' /
 * 'part-time' / '' or passes the rendered string through.
 */
export function decodeWorkload(value) {
    if (value == null || value === '') return '';
    const v = String(value);
    const suffix = v.includes('.') ? v.split('.').pop() : v;
    const lower = suffix.toLowerCase();
    if (lower === 'fulltime' || lower.includes('full')) return 'full-time';
    if (lower === 'parttime' || lower.includes('part')) return 'part-time';
    if (lower.includes('hrs/week') || lower.includes('hours')) return suffix.trim();
    return '';
}

/**
 * Decode `proposalsTier` to a compact bucket label. Search ships it as
 * `usnuxt_JobProposalTier_418.lessThan5` etc; feed ships it pre-rendered
 * as `15 to 20` / `5 to 10`. Returns the bucket like '<5' / '5-10' /
 * '20-50' / '50+', or '' if unrecognized.
 */
export function decodeProposalsTier(value) {
    if (value == null || value === '') return '';
    const v = String(value);
    const suffix = v.includes('.') ? v.split('.').pop() : v;
    const s = suffix.trim();
    if (/^lessThan(\d+)$/i.test(s)) return `<${s.match(/\d+/)[0]}`;
    if (/^(\d+)plus$/i.test(s)) return `${s.match(/\d+/)[0]}+`;
    const range = s.match(/^(\d+)\s*(?:to|-|–)\s*(\d+)$/i);
    if (range) return `${range[1]}-${range[2]}`;
    return s;
}

/**
 * Format the budget into a single human-readable column.
 *   - hourly (type 2):  "$40-$70/hr"  or "$30/hr"  or "" (no budget set)
 *   - fixed  (type 1):  "$200"  or "" (no amount)
 * Used by search/feed; detail uses formatBudgetFromDetail (different shape).
 */
export function formatBudget(job) {
    const type = job?.type;
    const min = Number(job?.hourlyBudget?.min) || 0;
    const max = Number(job?.hourlyBudget?.max) || 0;
    const amount = Number(job?.amount?.amount) || 0;
    if (type === 2) {
        if (min > 0 && max > 0 && max !== min) return `$${min}-$${max}/hr`;
        if (max > 0) return `$${max}/hr`;
        if (min > 0) return `$${min}/hr`;
        return '';
    }
    if (type === 1) return amount > 0 ? `$${amount}` : '';
    return '';
}

/** Detail page uses `extendedBudgetInfo.{hourlyBudgetMin,Max}` + `budget.amount`. */
export function formatBudgetFromDetail(job) {
    const type = job?.type;
    const min = Number(job?.extendedBudgetInfo?.hourlyBudgetMin) || 0;
    const max = Number(job?.extendedBudgetInfo?.hourlyBudgetMax) || 0;
    const amount = Number(job?.budget?.amount) || 0;
    if (type === 2) {
        if (min > 0 && max > 0 && max !== min) return `$${min}-$${max}/hr`;
        if (max > 0) return `$${max}/hr`;
        if (min > 0) return `$${min}/hr`;
        return '';
    }
    if (type === 1) return amount > 0 ? `$${amount}` : '';
    return '';
}

export function jobType(type) {
    if (type === 1) return 'fixed';
    if (type === 2) return 'hourly';
    return '';
}

/**
 * Join skills/attrs into a comma-separated string. Search rows have
 * `attrs[].prettyName`; feed rows additionally have `skills[].prefLabel`;
 * detail has neither (skills live elsewhere). The function picks
 * whichever array is populated and dedupes.
 */
export function formatSkills(job) {
    const candidates = [];
    const arrs = [job?.attrs, job?.skills, job?.ontologySkills];
    for (const arr of arrs) {
        if (!Array.isArray(arr)) continue;
        for (const s of arr) {
            const name = (s?.prettyName ?? s?.prefLabel ?? s?.name ?? '').trim();
            if (name && !candidates.includes(name)) candidates.push(name);
        }
    }
    return candidates.join(', ');
}

/**
 * Normalize a search/feed job entry into the shared LIST_COLUMNS row shape.
 * Returns null when the row lacks a round-trippable ciphertext identity.
 */
export function jobToListRow(job, rank) {
    const id = String(job?.ciphertext ?? '').trim();
    if (!isValidCiphertext(id)) return null;
    const client = job?.client || {};
    const country = client?.location?.country || '';
    const rating = Number(client?.totalFeedback);
    return {
        rank,
        id,
        title: stripHighlight(job?.title),
        type: jobType(job?.type),
        budget: formatBudget(job),
        experienceLevel: decodeExperienceLevel(job?.tierText ?? job?.tier),
        proposalsTier: decodeProposalsTier(job?.proposalsTier),
        skills: formatSkills(job),
        clientCountry: country,
        clientRating: Number.isFinite(rating) && rating > 0 ? rating : null,
        publishedOn: job?.publishedOn || job?.createdOn || '',
        url: buildJobUrl(id),
    };
}

export function jobsToListRows(jobs, { offset = 0, limit } = {}) {
    const rows = [];
    const source = limit ? jobs.slice(0, limit) : jobs;
    for (const [index, job] of source.entries()) {
        const rank = offset + index + 1;
        const row = jobToListRow(job, rank);
        if (!row) {
            throw new CommandExecutionError(`Upwork result at rank ${rank} did not include a valid ciphertext id; cannot produce round-trippable detail rows.`);
        }
        rows.push(row);
    }
    return rows;
}

export const LIST_COLUMNS = [
    'rank', 'id', 'title', 'type', 'budget',
    'experienceLevel', 'proposalsTier', 'skills',
    'clientCountry', 'clientRating', 'publishedOn', 'url',
];

export const DETAIL_COLUMNS = [
    'id', 'title', 'type', 'budget', 'experienceLevel', 'workload',
    'category', 'skills', 'description',
    'clientCountry', 'clientSpent', 'clientHires', 'clientRating',
    'proposalsCount', 'publishedOn', 'url',
];


// Copied verbatim from @upstash/context7-mcp (packages/mcp/src/lib/utils.ts)
// to keep pi's text output identical to what MCP produces. Update both together.

import type { SearchResponse, SearchResult } from "./types";

function getSourceReputationLabel(
  sourceReputation?: number
): "High" | "Medium" | "Low" | "Unknown" {
  if (sourceReputation === undefined || sourceReputation < 0) return "Unknown";
  if (sourceReputation >= 7) return "High";
  if (sourceReputation >= 4) return "Medium";
  return "Low";
}

export function formatSearchResult(result: SearchResult): string {
  const formattedResult = [
    `- Title: ${result.title}`,
    `- Context7-compatible library ID: ${result.id}`,
    `- Description: ${result.description}`,
  ];

  if (result.totalSnippets !== -1 && result.totalSnippets !== undefined) {
    formattedResult.push(`- Code Snippets: ${result.totalSnippets}`);
  }

  const reputationLabel = getSourceReputationLabel(result.trustScore);
  formattedResult.push(`- Source Reputation: ${reputationLabel}`);

  if (result.benchmarkScore !== undefined && result.benchmarkScore > 0) {
    formattedResult.push(`- Benchmark Score: ${result.benchmarkScore}`);
  }

  if (result.versions !== undefined && result.versions.length > 0) {
    formattedResult.push(`- Versions: ${result.versions.join(", ")}`);
  }

  if (result.source) {
    formattedResult.push(`- Source: ${result.source}`);
  }

  return formattedResult.join("\n");
}

export function formatSearchResults(searchResponse: SearchResponse): string {
  if (!searchResponse.results || searchResponse.results.length === 0) {
    return "No documentation libraries found matching your query.";
  }

  const parts: string[] = [];

  if (searchResponse.searchFilterApplied) {
    parts.push(
      "**Note:** Your results only include libraries matching your teamspace's library filters. To adjust quality thresholds or blocked libraries, update your filters at https://context7.com/dashboard?tab=policies"
    );
  }

  const formattedResults = searchResponse.results.map(formatSearchResult);
  parts.push(formattedResults.join("\n----------\n"));

  return parts.join("\n\n");
}


/*
 * Copyright 2024 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package com.netflix.maestro.engine.execution;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import com.netflix.maestro.engine.MaestroEngineBaseTest;
import com.netflix.maestro.engine.dao.MaestroStepInstanceDao;
import com.netflix.maestro.engine.db.DbOperation;
import com.netflix.maestro.models.error.Details;
import com.netflix.maestro.models.instance.StepInstance;
import com.netflix.maestro.queue.jobevents.MaestroJobEvent;
import com.netflix.maestro.queue.jobevents.NotificationJobEvent;
import com.netflix.maestro.queue.jobevents.StepInstanceUpdateJobEvent;
import java.util.Collections;
import java.util.Optional;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

public class StepSyncManagerTest extends MaestroEngineBaseTest {

  private final MaestroStepInstanceDao instanceDao = mock(MaestroStepInstanceDao.class);
  private final StepInstance instance = mock(StepInstance.class);
  private final WorkflowSummary workflowSummary = mock(WorkflowSummary.class);
  private final StepSyncManager syncManager = new StepSyncManager(instanceDao);

  @BeforeClass
  public static void init() {
    MaestroEngineBaseTest.init();
  }

  @Test
  public void testInsertSync() {
    StepRuntimeSummary stepRuntimeSummary =
        StepRuntimeSummary.builder()
            .stepId("test-summary")
            .stepAttemptId(2)
            .stepInstanceId(1)
            .dbOperation(DbOperation.INSERT)
            .build();
    Optional<Details> details = syncManager.sync(instance, workflowSummary, stepRuntimeSummary);
    assertFalse(details.isPresent());
    verify(instanceDao, times(1)).insertOrUpsertStepInstance(instance, false, null);
  }

  @Test
  public void testUpsertSync() {
    StepRuntimeSummary stepRuntimeSummary =
        StepRuntimeSummary.builder()
            .stepId("test-summary")
            .stepAttemptId(2)
            .stepInstanceId(1)
            .dbOperation(DbOperation.UPSERT)
            .build();
    Optional<Details> details = syncManager.sync(instance, workflowSummary, stepRuntimeSummary);
    assertFalse(details.isPresent());
    verify(instanceDao, times(1)).insertOrUpsertStepInstance(instance, true, null);
  }

  @Test
  public void testUpdateSync() {
    StepRuntimeSummary stepRuntimeSummary =
        StepRuntimeSummary.builder()
            .stepId("test-summary")
            .stepAttemptId(2)
            .stepInstanceId(1)
            .dbOperation(DbOperation.UPDATE)
            .build();
    Optional<Details> details = syncManager.sync(instance, workflowSummary, stepRuntimeSummary);
    assertFalse(details.isPresent());
    verify(instanceDao, times(1)).updateStepInstance(workflowSummary, stepRuntimeSummary, null);
  }

  @Test
  public void testInvalidDbOperation() {
    StepRuntimeSummary stepRuntimeSummary =
        StepRuntimeSummary.builder()
            .stepId("test-summary")
            .stepAttemptId(2)
            .stepInstanceId(1)
            .dbOperation(DbOperation.DELETE)
            .build();
    Optional<Details> details = syncManager.sync(instance, workflowSummary, stepRuntimeSummary);
    assertTrue(details.isPresent());
    assertEquals("Failed to sync a Maestro step state change", details.get().getMessage());
    assertFalse(details.get().getErrors().isEmpty());
    assertEquals(
        "MaestroInternalError: Invalid DB operation: DELETE for step instance [test-summary][2]",
        details.get().getErrors().get(0));
  }

  @Test
  public void testInsertPendingRecords() {
    StepRuntimeSummary stepRuntimeSummary =
        StepRuntimeSummary.builder()
            .stepId("test-summary")
            .stepAttemptId(2)
            .stepInstanceId(1)
            .dbOperation(DbOperation.INSERT)
            .pendingRecords(
                Collections.singletonList(
                    mock(StepInstanceUpdateJobEvent.StepInstancePendingRecord.class)))
            .build();
    Optional<Details> details = syncManager.sync(instance, workflowSummary, stepRuntimeSummary);
    assertFalse(details.isPresent());
    var eventCaptor = ArgumentCaptor.forClass(MaestroJobEvent.class);
    verify(instanceDao, times(1))
        .insertOrUpsertStepInstance(eq(instance), eq(false), eventCaptor.capture());
    assertEquals(StepInstanceUpdateJobEvent.class, eventCaptor.getValue().getClass());
  }

  @Test
  public void testUpdatePendingRecords() {
    StepRuntimeSummary stepRuntimeSummary =
        StepRuntimeSummary.builder()
            .stepId("test-summary")
            .stepAttemptId(2)
            .stepInstanceId(1)
            .dbOperation(DbOperation.UPDATE)
            .pendingRecords(
                Collections.singletonList(
                    mock(StepInstanceUpdateJobEvent.StepInstancePendingRecord.class)))
            .build();
    Optional<Details> details = syncManager.sync(instance, workflowSummary, stepRuntimeSummary);
    assertFalse(details.isPresent());
    var eventCaptor = ArgumentCaptor.forClass(MaestroJobEvent.class);
    verify(instanceDao, times(1))
        .updateStepInstance(eq(workflowSummary), eq(stepRuntimeSummary), eventCaptor.capture());
    assertEquals(NotificationJobEvent.class, eventCaptor.getValue().getClass());
  }

  @Test
  public void testSyncFailure() {
    doThrow(new RuntimeException("test error"))
        .when(instanceDao)
        .updateStepInstance(any(), any(), any());
    StepRuntimeSummary stepRuntimeSummary =
        StepRuntimeSummary.builder()
            .stepId("test-summary")
            .stepAttemptId(2)
            .stepInstanceId(1)
            .dbOperation(DbOperation.UPDATE)
            .pendingRecords(
                Collections.singletonList(
                    mock(StepInstanceUpdateJobEvent.StepInstancePendingRecord.class)))
            .build();
    Optional<Details> details = syncManager.sync(instance, workflowSummary, stepRuntimeSummary);
    assertTrue(details.isPresent());
    assertEquals("Failed to sync a Maestro step state change", details.get().getMessage());
  }
}


use async_stream::stream;
use futures::{Stream, StreamExt};
use serde::{Deserialize, Serialize};
use std::pin::Pin;
use tracing::{Level, enabled};
use tracing_futures::Instrument;

use super::InteractionsCompletionModel;
use super::create_request_body;
use super::interactions_api_types::{
    Content, ContentDelta, FunctionCallContent, FunctionCallDelta, Interaction,
    InteractionSseEvent, InteractionUsage, Step, TextDelta, ThoughtSummaryContent,
    ThoughtSummaryDelta,
};
use crate::completion::{CompletionError, CompletionRequest, GetTokenUsage};
use crate::http_client::HttpClientExt;
use crate::http_client::Request;
use crate::http_client::sse::{Event, GenericEventSource};
use crate::streaming;
use crate::telemetry::{CompletionOperation, CompletionSpanBuilder, SpanCombinator};
use serde_json::{Map, Value};

/// Final metadata yielded by an Interactions streaming response.
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct StreamingCompletionResponse {
    pub usage: Option<InteractionUsage>,
    pub interaction: Option<Interaction>,
    /// Resolved model identifier (e.g. `gemini-2.5-pro-preview-05-06`), extracted from
    /// `Interaction.model`. The Interactions API has no `FinishReason` field; use
    /// `interaction.status` for lifecycle state.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_version: Option<String>,
}

#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
pub type InteractionEventStream =
    Pin<Box<dyn Stream<Item = Result<InteractionSseEvent, CompletionError>> + Send>>;

#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
pub type InteractionEventStream =
    Pin<Box<dyn Stream<Item = Result<InteractionSseEvent, CompletionError>>>>;

impl GetTokenUsage for StreamingCompletionResponse {
    fn token_usage(&self) -> crate::completion::Usage {
        self.usage
            .as_ref()
            .map(|usage| usage.token_usage())
            .unwrap_or_default()
    }
}

impl<T> InteractionsCompletionModel<T>
where
    T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
{
    pub(crate) async fn stream(
        &self,
        completion_request: CompletionRequest,
    ) -> Result<streaming::StreamingCompletionResponse<StreamingCompletionResponse>, CompletionError>
    {
        let span = CompletionSpanBuilder::new(
            "gcp.gemini",
            &self.model,
            CompletionOperation::InteractionsStreaming,
        )
        .system_instructions(
            completion_request.preamble.as_deref(),
            completion_request.record_telemetry_content,
        )
        .build();

        let request = create_request_body(self.model.clone(), completion_request, Some(true))?;

        if enabled!(Level::TRACE) {
            tracing::trace!(
                target: "rig::streaming",
                "Gemini interactions streaming request: {}",
                serde_json::to_string_pretty(&request)?
            );
        }

        let body = serde_json::to_vec(&request)?;
        let req = self
            .client
            .post_sse("/v1beta/interactions")?
            .header("Content-Type", "application/json")
            .body(body)
            .map_err(|e| CompletionError::HttpError(e.into()))?;

        let mut event_source = GenericEventSource::new(self.client.clone(), req);

        let stream = stream! {
            let mut final_interaction: Option<Interaction> = None;
            let mut final_usage: Option<InteractionUsage> = None;

            while let Some(event_result) = event_source.next().await {
                match event_result {
                    Ok(Event::Open) => {
                        tracing::debug!("SSE connection opened");
                        continue;
                    }
                    Ok(Event::Message(message)) => {
                        if message.data.trim().is_empty() {
                            continue;
                        }

                        let data = match serde_json::from_str::<InteractionSseEvent>(&message.data)
                        {
                            Ok(data) => data,
                            Err(err) => {
                                tracing::debug!(
                                    "Failed to deserialize interactions SSE event: {err}"
                                );
                                continue;
                            }
                        };

                        match data {
                            InteractionSseEvent::StepDelta { delta, .. } => {
                                if let Some(choice) = content_delta_to_choice(delta) {
                                    yield Ok(choice);
                                }
                            }
                            InteractionSseEvent::StepStart { step, .. } => {
                                if let Some(choice) = step_start_to_choice(step) {
                                    yield Ok(choice);
                                }
                            }
                            InteractionSseEvent::InteractionCompleted { interaction, .. } => {
                                let span = tracing::Span::current();
                                span.record("gen_ai.response.id", &interaction.id);
                                if let Some(model) = interaction.model.clone() {
                                    span.record("gen_ai.response.model", model);
                                }

                                if let Some(usage) = interaction.usage.clone() {
                                    span.record_token_usage(&usage);
                                    final_usage = Some(usage);
                                }
                                final_interaction = Some(interaction);
                            }
                            InteractionSseEvent::Error { .. } => {
                                // Preserve the full provider error payload (code +
                                // message) by reusing the raw SSE event JSON, matching
                                // the SSE path's `completion_error_from_body`. The error
                                // arrives over an established stream, so there is no HTTP
                                // status to attach (status: None).
                                yield Err(crate::provider_response::completion_error_from_body(
                                    message.data,
                                ));
                                break;
                            }
                            _ => continue,
                        }
                    }
                    Err(crate::http_client::Error::StreamEnded) => {
                        break;
                    }
                    Err(error) => {
                        tracing::error!(?error, "SSE error");
                        yield Err(CompletionError::from_stream_transport(error));
                        break;
                    }
                }
            }

            event_source.close();

            let model_version = final_interaction.as_ref().and_then(|i| i.model.clone());
            yield Ok(streaming::RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
                usage: final_usage.or_else(|| final_interaction.as_ref().and_then(|i| i.usage.clone())),
                interaction: final_interaction,
                model_version,
            }));
        }
        .instrument(span);

        Ok(streaming::StreamingCompletionResponse::stream(Box::pin(
            stream,
        )))
    }
}

pub(crate) fn stream_interaction_events<T>(
    client: super::InteractionsClient<T>,
    request: Request<Vec<u8>>,
) -> InteractionEventStream
where
    T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
{
    let mut event_source = GenericEventSource::new(client.clone(), request);

    let stream = stream! {
        while let Some(event_result) = event_source.next().await {
            match event_result {
                Ok(Event::Open) => continue,
                Ok(Event::Message(message)) => {
                    if message.data.trim().is_empty() {
                        continue;
                    }

                    let data = serde_json::from_str::<InteractionSseEvent>(&message.data);
                    let Ok(data) = data else {
                        let Err(err) = data else {
                            continue;
                        };
                        tracing::debug!("Failed to deserialize interactions SSE event: {err}");
                        continue;
                    };

                    yield Ok(data);
                }
                Err(crate::http_client::Error::StreamEnded) => break,
                Err(error) => {
                    tracing::error!(?error, "SSE error");
                    yield Err(CompletionError::from_stream_transport(error));
                    break;
                }
            }
        }

        event_source.close();
    };

    Box::pin(stream)
}

fn step_start_to_choice(
    step: Step,
) -> Option<streaming::RawStreamingChoice<StreamingCompletionResponse>> {
    match step {
        Step::ModelOutput { content } => content.into_iter().find_map(content_to_choice),
        Step::FunctionCall(FunctionCallContent {
            name,
            arguments,
            id,
        }) => {
            let name = name?;
            let call_id = id.unwrap_or_else(|| name.clone());
            Some(streaming::RawStreamingChoice::ToolCall(
                streaming::RawStreamingToolCall::new(
                    name.clone(),
                    name,
                    arguments.unwrap_or(Value::Object(Map::new())),
                )
                .with_call_id(call_id),
            ))
        }
        _ => None,
    }
}

fn content_to_choice(
    content: Content,
) -> Option<streaming::RawStreamingChoice<StreamingCompletionResponse>> {
    match content {
        Content::Text(text) if !text.text.is_empty() => {
            Some(streaming::RawStreamingChoice::Message(text.text))
        }
        Content::FunctionCall(content) => step_start_to_choice(Step::FunctionCall(content)),
        _ => None,
    }
}

fn content_delta_to_choice(
    delta: ContentDelta,
) -> Option<streaming::RawStreamingChoice<StreamingCompletionResponse>> {
    match delta {
        ContentDelta::Text(TextDelta {
            text: Some(text), ..
        }) => Some(streaming::RawStreamingChoice::Message(text)),
        ContentDelta::FunctionCall(FunctionCallDelta {
            name,
            arguments,
            id,
        }) => {
            let name = name?;
            let call_id = id.unwrap_or_else(|| name.clone());
            Some(streaming::RawStreamingChoice::ToolCall(
                streaming::RawStreamingToolCall::new(
                    name.clone(),
                    name,
                    arguments.unwrap_or(Value::Object(Map::new())),
                )
                .with_call_id(call_id),
            ))
        }
        ContentDelta::ThoughtSummary(ThoughtSummaryDelta { content }) => {
            let text = match content {
                ThoughtSummaryContent::Text(text) => text.text,
                _ => return None,
            };
            Some(streaming::RawStreamingChoice::ReasoningDelta {
                id: None,
                reasoning: text,
            })
        }
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_streaming_completion_response_has_model_version() {
        let response = StreamingCompletionResponse {
            usage: None,
            interaction: None,
            model_version: Some("gemini-2.5-pro-preview-05-06".to_string()),
        };

        assert_eq!(
            response.model_version.as_deref(),
            Some("gemini-2.5-pro-preview-05-06")
        );

        let json = serde_json::to_string(&response).unwrap();
        let deserialized: StreamingCompletionResponse = serde_json::from_str(&json).unwrap();
        assert_eq!(
            deserialized.model_version.as_deref(),
            Some("gemini-2.5-pro-preview-05-06")
        );
    }

    #[test]
    fn test_content_delta_text_event() {
        let event_json = json!({
            "event_type": "step.delta",
            "index": 0,
            "delta": {
                "type": "text",
                "text": "Hello"
            }
        });

        let event: InteractionSseEvent = serde_json::from_value(event_json).unwrap();
        let InteractionSseEvent::StepDelta { delta, .. } = event else {
            panic!("expected step delta");
        };

        let choice = content_delta_to_choice(delta).expect("choice should exist");
        match choice {
            crate::streaming::RawStreamingChoice::Message(text) => {
                assert_eq!(text, "Hello");
            }
            other => panic!("unexpected choice: {other:?}"),
        }
    }

    #[test]
    fn test_content_delta_function_call_event() {
        let event_json = json!({
            "event_type": "step.delta",
            "index": 0,
            "delta": {
                "type": "function_call",
                "name": "get_weather",
                "arguments": {"location": "Paris"},
                "id": "call-1"
            }
        });

        let event: InteractionSseEvent = serde_json::from_value(event_json).unwrap();
        let InteractionSseEvent::StepDelta { delta, .. } = event else {
            panic!("expected step delta");
        };

        let choice = content_delta_to_choice(delta).expect("choice should exist");
        match choice {
            crate::streaming::RawStreamingChoice::ToolCall(call) => {
                assert_eq!(call.name, "get_weather");
                assert_eq!(call.call_id.as_deref(), Some("call-1"));
            }
            other => panic!("unexpected choice: {other:?}"),
        }
    }
}


import sys
import io
from warnings import warn

from typing import BinaryIO, Any

from ._html_converter import HtmlConverter
from ..converter_utils.docx.pre_process import pre_process_docx
from .._base_converter import DocumentConverterResult
from .._stream_info import StreamInfo
from .._exceptions import MissingDependencyException, MISSING_DEPENDENCY_MESSAGE

# Try loading optional (but in this case, required) dependencies
# Save reporting of any exceptions for later
_dependency_exc_info = None
try:
    import mammoth

except ImportError:
    # Preserve the error and stack trace for later
    _dependency_exc_info = sys.exc_info()

ACCEPTED_MIME_TYPE_PREFIXES = [
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
]

ACCEPTED_FILE_EXTENSIONS = [".docx"]

class DocxConverter(HtmlConverter):
    """
    Converts DOCX files to Markdown. Style information (e.g., headings) and tables are preserved where possible.
    """

    def __init__(self):
        super().__init__()
        self._html_converter = HtmlConverter()

    def accepts(
        self,
        file_stream: BinaryIO,
        stream_info: StreamInfo,
        **kwargs: Any,  # Options to pass to the converter
    ) -> bool:
        mimetype = (stream_info.mimetype or "").lower()
        extension = (stream_info.extension or "").lower()

        if extension in ACCEPTED_FILE_EXTENSIONS:
            return True

        for prefix in ACCEPTED_MIME_TYPE_PREFIXES:
            if mimetype.startswith(prefix):
                return True

        return False

    def convert(
        self,
        file_stream: BinaryIO,
        stream_info: StreamInfo,
        **kwargs: Any,  # Options to pass to the converter
    ) -> DocumentConverterResult:
        # Check: the dependencies
        if _dependency_exc_info is not None:
            raise MissingDependencyException(
                MISSING_DEPENDENCY_MESSAGE.format(
                    converter=type(self).__name__,
                    extension=".docx",
                    feature="docx",
                )
            ) from _dependency_exc_info[
                1
            ].with_traceback(  # type: ignore[union-attr]
                _dependency_exc_info[2]
            )

        style_map = kwargs.get("style_map", None)
        pre_process_stream = pre_process_docx(file_stream)
        return self._html_converter.convert_string(
            mammoth.convert_to_html(pre_process_stream, style_map=style_map).value,
            **kwargs,
        )


//! Live Bedrock Anthropic adaptive-thinking regression tests.

use futures::StreamExt;
use rig::agent::AgentBuilder;
use rig::completion::Prompt;
use rig::prelude::*;
use rig::streaming::StreamedAssistantContent;
use serde_json::json;

use super::{
    anthropic_adaptive_model, anthropic_signature_only_model, client,
    support::{ALPHA_SIGNAL_OUTPUT, AlphaSignal, assert_contains_all_case_insensitive},
};

fn adaptive_thinking_params() -> serde_json::Value {
    json!({
        "thinking": {
            "type": "adaptive"
        }
    })
}

#[tokio::test]
#[ignore = "requires AWS credentials and Bedrock Anthropic adaptive-thinking model access"]
async fn adaptive_thinking_prompt_caching_tool_roundtrip_regression() {
    let model = client()
        .completion_model(anthropic_adaptive_model())
        .with_prompt_caching();
    let agent = AgentBuilder::new(model)
        .preamble(
            "You must call tools when the user asks for their result. \
             After a tool result is available, answer with the exact result.",
        )
        .max_tokens(2048)
        .additional_params(adaptive_thinking_params())
        .tool(AlphaSignal)
        .build();

    let response = agent
        .prompt("Call `lookup_harbor_label` exactly once, then answer with the exact tool output.")
        .await
        .expect("adaptive-thinking prompt-caching tool roundtrip should succeed");

    assert_contains_all_case_insensitive(&response, &[ALPHA_SIGNAL_OUTPUT]);
}

#[tokio::test]
#[ignore = "requires AWS credentials and Bedrock Anthropic adaptive-thinking model access"]
async fn streaming_emits_signature_only_adaptive_reasoning_regression() {
    let model = client().completion_model(anthropic_signature_only_model());
    let request = model
        .completion_request("What is 2 + 2? Answer with only the number.")
        .max_tokens(2048)
        .additional_params(adaptive_thinking_params())
        .build();
    let mut stream = model
        .stream(request)
        .await
        .expect("adaptive-thinking Bedrock stream should start");

    let mut reasoning_chunks = 0;
    let mut signature_chunks = 0;
    let mut signature_only_chunks = 0;
    let mut got_final = false;

    while let Some(item) = stream.next().await {
        match item.expect("adaptive-thinking Bedrock stream item should succeed") {
            StreamedAssistantContent::Reasoning(reasoning) => {
                reasoning_chunks += 1;
                if reasoning.first_signature().is_some() {
                    signature_chunks += 1;
                    if reasoning.display_text().is_empty() {
                        signature_only_chunks += 1;
                    }
                }
            }
            StreamedAssistantContent::Final(_) => got_final = true,
            _ => {}
        }
    }

    assert!(got_final, "stream should emit a final response");
    assert!(
        reasoning_chunks > 0,
        "expected at least one adaptive-thinking reasoning chunk"
    );
    assert!(
        signature_chunks > 0,
        "expected adaptive-thinking reasoning to include a Bedrock signature"
    );
    assert!(
        signature_only_chunks > 0,
        "expected at least one signature-only reasoning chunk"
    );
}


# @file purpose: Observability module for browser-use that handles optional lmnr integration with debug mode support
"""
Observability module for browser-use

This module provides observability decorators that optionally integrate with lmnr (Laminar) for tracing.
If lmnr is not installed, it provides no-op wrappers that accept the same parameters.

Features:
- Optional lmnr integration - works with or without lmnr installed
- Debug mode support - observe_debug only traces when in debug mode
- Full parameter compatibility with lmnr observe decorator
- No-op fallbacks when lmnr is unavailable
"""

import logging
import os
from collections.abc import Callable
from functools import wraps
from typing import Any, Literal, TypeVar, cast

logger = logging.getLogger(__name__)
from dotenv import load_dotenv

load_dotenv()

# Type definitions
F = TypeVar('F', bound=Callable[..., Any])

# Check if we're in debug mode
def _is_debug_mode() -> bool:
	"""Check if we're in debug mode based on environment variables or logging level."""

	lmnr_debug_mode = os.getenv('LMNR_LOGGING_LEVEL', '').lower()
	if lmnr_debug_mode == 'debug':
		# logger.info('Debug mode is enabled for observability')
		return True
	# logger.info('Debug mode is disabled for observability')
	return False

# Try to import lmnr observe
_LMNR_AVAILABLE = False
_lmnr_observe = None

try:
	from lmnr import observe as _lmnr_observe  # type: ignore

	if os.environ.get('BROWSER_USE_VERBOSE_OBSERVABILITY', 'false').lower() == 'true':
		logger.debug('Lmnr is available for observability')
	_LMNR_AVAILABLE = True
except (ImportError, TypeError):
	if os.environ.get('BROWSER_USE_VERBOSE_OBSERVABILITY', 'false').lower() == 'true':
		logger.debug('Lmnr is not available for observability')
	_LMNR_AVAILABLE = False

def _create_no_op_decorator(
	name: str | None = None,
	ignore_input: bool = False,
	ignore_output: bool = False,
	metadata: dict[str, Any] | None = None,
	**kwargs: Any,
) -> Callable[[F], F]:
	"""Create a no-op decorator that accepts all lmnr observe parameters but does nothing."""
	import asyncio

	def decorator(func: F) -> F:
		if asyncio.iscoroutinefunction(func):

			@wraps(func)
			async def async_wrapper(*args, **kwargs):
				return await func(*args, **kwargs)

			return cast(F, async_wrapper)
		else:

			@wraps(func)
			def sync_wrapper(*args, **kwargs):
				return func(*args, **kwargs)

			return cast(F, sync_wrapper)

	return decorator

def observe(
	name: str | None = None,
	ignore_input: bool = False,
	ignore_output: bool = False,
	metadata: dict[str, Any] | None = None,
	span_type: Literal['DEFAULT', 'LLM', 'TOOL'] = 'DEFAULT',
	**kwargs: Any,
) -> Callable[[F], F]:
	"""
	Observability decorator that traces function execution when lmnr is available.

	This decorator will use lmnr's observe decorator if lmnr is installed,
	otherwise it will be a no-op that accepts the same parameters.

	Args:
	    name: Name of the span/trace
	    ignore_input: Whether to ignore function input parameters in tracing
	    ignore_output: Whether to ignore function output in tracing
	    metadata: Additional metadata to attach to the span
	    **kwargs: Additional parameters passed to lmnr observe

	Returns:
	    Decorated function that may be traced depending on lmnr availability

	Example:
	    @observe(name="my_function", metadata={"version": "1.0"})
	    def my_function(param1, param2):
	        return param1 + param2
	"""
	kwargs = {
		'name': name,
		'ignore_input': ignore_input,
		'ignore_output': ignore_output,
		'metadata': metadata,
		'span_type': span_type,
		'tags': ['observe', 'observe_debug'],  # important: tags need to be created on laminar first
		**kwargs,
	}

	if _LMNR_AVAILABLE and _lmnr_observe:
		# Use the real lmnr observe decorator
		return cast(Callable[[F], F], _lmnr_observe(**kwargs))
	else:
		# Use no-op decorator
		return _create_no_op_decorator(**kwargs)

def observe_debug(
	name: str | None = None,
	ignore_input: bool = False,
	ignore_output: bool = False,
	metadata: dict[str, Any] | None = None,
	span_type: Literal['DEFAULT', 'LLM', 'TOOL'] = 'DEFAULT',
	**kwargs: Any,
) -> Callable[[F], F]:
	"""
	Debug-only observability decorator that only traces when in debug mode.

	This decorator will use lmnr's observe decorator if both lmnr is installed
	AND we're in debug mode, otherwise it will be a no-op.

	Debug mode is determined by:
	- DEBUG environment variable set to 1/true/yes/on
	- BROWSER_USE_DEBUG environment variable set to 1/true/yes/on
	- Root logging level set to DEBUG or lower

	Args:
	    name: Name of the span/trace
	    ignore_input: Whether to ignore function input parameters in tracing
	    ignore_output: Whether to ignore function output in tracing
	    metadata: Additional metadata to attach to the span
	    **kwargs: Additional parameters passed to lmnr observe

	Returns:
	    Decorated function that may be traced only in debug mode

	Example:
	    @observe_debug(ignore_input=True, ignore_output=True,name="debug_function", metadata={"debug": True})
	    def debug_function(param1, param2):
	        return param1 + param2
	"""
	kwargs = {
		'name': name,
		'ignore_input': ignore_input,
		'ignore_output': ignore_output,
		'metadata': metadata,
		'span_type': span_type,
		'tags': ['observe_debug'],  # important: tags need to be created on laminar first
		**kwargs,
	}

	if _LMNR_AVAILABLE and _lmnr_observe and _is_debug_mode():
		# Use the real lmnr observe decorator only in debug mode
		return cast(Callable[[F], F], _lmnr_observe(**kwargs))
	else:
		# Use no-op decorator (either not in debug mode or lmnr not available)
		return _create_no_op_decorator(**kwargs)

# Convenience functions for checking availability and debug status
def is_lmnr_available() -> bool:
	"""Check if lmnr is available for tracing."""
	return _LMNR_AVAILABLE

def is_debug_mode() -> bool:
	"""Check if we're currently in debug mode."""
	return _is_debug_mode()

def get_observability_status() -> dict[str, bool]:
	"""Get the current status of observability features."""
	return {
		'lmnr_available': _LMNR_AVAILABLE,
		'debug_mode': _is_debug_mode(),
		'observe_active': _LMNR_AVAILABLE,
		'observe_debug_active': _LMNR_AVAILABLE and _is_debug_mode(),
	}


import re
import pytest
from unittest.mock import Mock

from scrapling import Selector, Selectors
from scrapling.core.custom_types import TextHandler, TextHandlers
from scrapling.core.storage import SQLiteStorageSystem

class TestSelectorAdvancedFeatures:
    """Test advanced Selector features like adaptive matching"""

    def test_adaptive_initialization_with_storage(self):
        """Test adaptive initialization with custom storage"""
        html = "
Test
"

        # Use the actual SQLiteStorageSystem for this test
        selector = Selector(
            content=html,
            adaptive=True,
            storage=SQLiteStorageSystem,
            storage_args={"storage_file": ":memory:", "url": "https://example.com"}
        )

        assert selector._Selector__adaptive_enabled is True
        assert selector._storage is not None

    def test_adaptive_initialization_with_default_storage_args(self):
        """Test adaptive initialization with default storage args"""
        html = "
Test
"
        url = "https://example.com"

        # Test that adaptive mode uses default storage when no explicit args provided
        selector = Selector(
            content=html,
            url=url,
            adaptive=True
        )

        # Should create storage with default args
        assert selector._storage is not None

    def test_adaptive_with_existing_storage(self):
        """Test adaptive initialization with existing storage object"""
        html = "
Test
"

        mock_storage = Mock()

        selector = Selector(
            content=html,
            adaptive=True,
            _storage=mock_storage
        )

        assert selector._storage is mock_storage

class TestAdvancedSelectors:
    """Test advanced selector functionality"""

    @pytest.fixture
    def complex_html(self):
        return """

First paragraph

Second paragraph

                    <![CDATA[Some CDATA content]]>

                        Special content
                        Regular content

Cell 1	Cell 2

Cell 3	Cell 4

        """

    def test_comment_and_cdata_handling(self, complex_html):
        """Test handling of comments and CDATA"""
        # With comments/CDATA kept
        page = Selector(
            complex_html,
            keep_comments=True,
            keep_cdata=True
        )
        content = page.body
        assert "Comment" in content
        assert "CDATA" in content

        # Without comments/CDATA
        page = Selector(
            complex_html,
            keep_comments=False,
            keep_cdata=False
        )
        content = page.html_content
        assert "Comment" not in content

    def test_advanced_xpath_variables(self, complex_html):
        """Test XPath with variables"""
        page = Selector(complex_html)

        # Using XPath variables
        cells = page.xpath(
            "//td[text()=$cell_text]",
            cell_text="Cell 1"
        )
        assert len(cells) == 1
        assert cells[0].text == "Cell 1"

    def test_pseudo_elements(self, complex_html):
        """Test CSS pseudo-elements"""
        page = Selector(complex_html)

        # ::text pseudo-element
        texts = page.css("p::text")
        assert len(texts) == 2
        assert isinstance(texts[0], Selector)
        assert isinstance(texts[0].get(), TextHandler)

        # ::attr() pseudo-element
        attrs = page.css("div::attr(class)")
        assert "container" in attrs.getall()

    def test_complex_attribute_operations(self, complex_html):
        """Test complex attribute handling"""
        page = Selector(complex_html)
        container = page.css(".container")[0]

        # JSON in attributes
        data = container.attrib["data-test"].json()
        assert data["key"] == "value"

        # Attribute searching
        matches = list(container.attrib.search_values("container"))
        assert len(matches) == 1

    def test_url_joining(self):
        """Test URL joining functionality"""
        page = Selector("", url="https://example.com/page")

        # Relative URL
        assert page.urljoin("../other") == "https://example.com/other"
        assert page.urljoin("/absolute") == "https://example.com/absolute"
        assert page.urljoin("relative") == "https://example.com/relative"

    def test_find_operations_edge_cases(self, complex_html):
        """Test edge cases in find operations"""
        page = Selector(complex_html)

        # Multiple argument types
        _ = page.find_all(
            "span",
            ["div"],
            {"class": "nested"},
            lambda e: e.text != ""
        )

        # Regex pattern matching
        pattern = re.compile(r"Cell \d+")
        cells = page.find_all(pattern)
        assert len(cells) == 4

    def test_text_operations_edge_cases(self, complex_html):
        """Test text operation edge cases"""
        page = Selector(complex_html)

        # get_all_text with a custom separator
        text = page.get_all_text(separator=" | ", strip=True)
        assert " | " in text

        # Ignore specific tags
        text = page.get_all_text(ignore_tags=("table",))
        assert "Cell" not in text

        # With empty values
        text = page.get_all_text(valid_values=False)
        assert text != ""

    def test_get_all_text_preserves_interleaved_text_nodes(self):
        """Test get_all_text preserves interleaved text nodes"""
        html = """

                string1
                string2
                string3

                    string4

                string5

                string6

                string7

        """

        page = Selector(html, adaptive=False)
        node = page.css("main")[0]

        assert node.get_all_text("\n", strip=True) == "string1\nstring2\nstring3\nstring4\nstring5\nstring6\nstring7"

class TestTextHandlerAdvanced:
    """Test advanced TextHandler functionality"""

    def test_text_handler_operations(self):
        """Test various TextHandler operations"""
        text = TextHandler("  Hello World  ")

        # All string methods should return TextHandler
        assert isinstance(text.strip(), TextHandler)
        assert isinstance(text.upper(), TextHandler)
        assert isinstance(text.lower(), TextHandler)
        assert isinstance(text.replace("World", "Python"), TextHandler)

        # Custom methods
        assert text.clean() == "Hello World"

        # Sorting
        text2 = TextHandler("dcba")
        assert text2.sort() == "abcd"

    def test_text_handler_regex(self):
        """Test regex operations on TextHandler"""
        text = TextHandler("Price: $10.99, Sale: $8.99")

        # Basic regex
        prices = text.re(r"\$[\d.]+")
        assert len(prices) == 2
        assert prices[0] == "$10.99"

        # Case insensitive
        text2 = TextHandler("HELLO hello HeLLo")
        matches = text2.re(r"hello", case_sensitive=False)
        assert len(matches) == 3

        # Clean match
        text3 = TextHandler(" He  l  lo  ")
        matches = text3.re(r"He l lo", clean_match=True, case_sensitive=False)
        assert len(matches) == 1

    def test_text_handler_regex_check_match(self):
        """Test TextHandler.re() with check_match=True returns bool"""
        text = TextHandler("Price: $10.99")
        assert text.re(r"\$[\d.]+", check_match=True) is True
        assert text.re(r"no-match-pattern", check_match=True) is False

    def test_text_handler_regex_replace_entities_false(self):
        """Test TextHandler.re() with replace_entities=False preserves entities"""
        text = TextHandler("Hello & World")
        results = text.re(r"&", replace_entities=False)
        assert len(results) == 1
        assert results[0] == "&"

    def test_text_handler_regex_with_groups(self):
        """Test TextHandler.re() with capture groups flattens results"""
        text = TextHandler("name=Alice age=30 name=Bob age=25")
        results = text.re(r"name=(\w+) age=(\d+)")
        assert len(results) == 4
        assert "Alice" in results
        assert "30" in results

    def test_text_handler_re_first_with_default(self):
        """Test TextHandler.re_first() returns default when no match"""
        text = TextHandler("no numbers here")
        result = text.re_first(r"\d+", default="N/A")
        assert result == "N/A"

    def test_text_handler_re_first_returns_first_match(self):
        """Test TextHandler.re_first() returns first match"""
        text = TextHandler("a1 b2 c3")
        result = text.re_first(r"\d")
        assert result == "1"
        assert isinstance(result, TextHandler)

    def test_text_handler_clean_with_entities(self):
        """Test TextHandler.clean() with remove_entities=True"""
        text = TextHandler("Hello\t&\nWorld")
        cleaned = text.clean(remove_entities=True)
        assert "&" not in cleaned
        assert "&" in cleaned
        assert "\t" not in cleaned
        assert "\n" not in cleaned

    def test_text_handler_clean_without_entities(self):
        """Test TextHandler.clean() preserves entities by default"""
        text = TextHandler("Hello\t&\nWorld")
        cleaned = text.clean(remove_entities=False)
        assert "&" in cleaned

    def test_text_handler_json_valid(self):
        """Test TextHandler.json() with valid JSON"""
        text = TextHandler('{"key": "value", "num": 42}')
        data = text.json()
        assert data["key"] == "value"
        assert data["num"] == 42

    def test_text_handler_json_invalid(self):
        """Test TextHandler.json() raises on invalid JSON"""
        text = TextHandler("not json")
        with pytest.raises(Exception):
            text.json()

    def test_text_handlers_operations(self):
        """Test TextHandlers list operations"""
        handlers = TextHandlers([
            TextHandler("First"),
            TextHandler("Second"),
            TextHandler("Third")
        ])

        # Slicing should return TextHandlers
        assert isinstance(handlers[0:2], TextHandlers)

        # Get methods
        assert handlers.get() == "First"
        assert handlers.get("default") == "First"
        assert TextHandlers([]).get("default") == "default"

    def test_text_handlers_re(self):
        """Test TextHandlers.re() flattens results across all elements"""
        handlers = TextHandlers([
            TextHandler("a1 b2"),
            TextHandler("c3 d4"),
        ])
        results = handlers.re(r"[a-z]\d")
        assert isinstance(results, TextHandlers)
        assert len(results) == 4
        assert results[0] == "a1"
        assert results[3] == "d4"

    def test_text_handlers_re_empty(self):
        """Test TextHandlers.re() on empty list"""
        handlers = TextHandlers([])
        results = handlers.re(r"\d+")
        assert isinstance(results, TextHandlers)
        assert len(results) == 0

    def test_text_handlers_re_no_matches(self):
        """Test TextHandlers.re() when no element matches"""
        handlers = TextHandlers([TextHandler("abc"), TextHandler("def")])
        results = handlers.re(r"\d+")
        assert len(results) == 0

    def test_text_handlers_extract(self):
        """Test TextHandlers.extract() returns self"""
        handlers = TextHandlers([TextHandler("a"), TextHandler("b")])
        assert handlers.extract() is handlers
        assert handlers.getall() is handlers

class TestSelectorsAdvanced:
    """Test advanced Selectors functionality"""

    def test_selectors_filtering(self):
        """Test filtering operations on Selectors"""
        html = """

Important

Regular

Also important

        """
        page = Selector(html)
        paragraphs = page.css("p")

        # Filter by class
        highlighted = paragraphs.filter(lambda p: p.has_class("highlight"))
        assert len(highlighted) == 2

        # Search for a specific element
        found = paragraphs.search(lambda p: p.text == "Regular")
        assert found is not None
        assert found.text == "Regular"

    def test_selectors_properties(self):
        """Test Selectors properties"""
        html = "

1

2

3

"
        page = Selector(html)
        paragraphs = page.css("p")

        assert paragraphs.first.text == "1"
        assert paragraphs.last.text == "3"
        assert paragraphs.length == 3


// Regression for #565: `irm .../install.ps1 | iex` crashed with
// "Cannot bind argument to parameter 'Path' because it is null."
//
// Two pipe-execution rules for install.ps1 (static checks — CI has no pwsh):
//   1. No top-level param() block. iex executes the file as a string, so a
//      top-level param can never receive arguments and (depending on host)
//      trips parsing. All logic lives in a function invoked at the bottom.
//   2. Script-path variables ($PSCommandPath / $MyInvocation.MyCommand.Path)
//      are $null under iex — any use must be guarded, never passed straight
//      into Split-Path (that was the #565 crash).

import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '..', '..');
const PS1 = fs.readFileSync(path.join(REPO_ROOT, 'install.ps1'), 'utf8');

// Strip comment lines so doc mentions of param()/path vars don't false-positive.
const code = PS1.split('\n').filter(l => !/^\s*#/.test(l)).join('\n');

test('#565 install.ps1 has no top-level param block (everything inside a function)', () => {
  const beforeFunction = code.slice(0, code.indexOf('function '));
  assert.ok(code.includes('function '), 'install.ps1 must wrap its logic in a function for iex piping');
  assert.ok(
    !/param\s*\(/i.test(beforeFunction),
    'install.ps1 must not declare a top-level param() — it cannot receive args under `irm | iex` (issue #565)',
  );
});

test('#565 install.ps1 never uses $MyInvocation.MyCommand.Path (null under iex)', () => {
  assert.ok(
    !/\$MyInvocation\.MyCommand\.Path/i.test(code),
    'install.ps1 must not rely on $MyInvocation.MyCommand.Path — it is $null when piped to iex (issue #565)',
  );
});

test('#565 install.ps1 guards $PSCommandPath before Split-Path', () => {
  if (/\$PSCommandPath/i.test(code)) {
    assert.match(
      code,
      /if\s*\(\s*\$PSCommandPath\s*\)/i,
      '$PSCommandPath is $null under `irm | iex` — it must be truthiness-guarded before use (issue #565)',
    );
  }
});

test('#565 install.ps1 invokes its function at the bottom (script still does something)', () => {
  const lastLines = code.trim().split('\n').slice(-3).join('\n');
  assert.match(
    lastLines,
    /Install-Caveman/,
    'install.ps1 must actually invoke Install-Caveman after defining it',
  );
});


import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "#/lib/utils.ts"
import { Label } from "#/components/ui/label.tsx"
import { Separator } from "#/components/ui/separator.tsx"

function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
  return (
    <fieldset
      data-slot="field-set"
      className={cn(
        "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
        className
      )}
      {...props}
    />
  )
}

function FieldLegend({
  className,
  variant = "legend",
  ...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
  return (
    <legend
      data-slot="field-legend"
      data-variant={variant}
      className={cn(
        "mb-2 font-medium data-[variant=label]:text-xs/relaxed data-[variant=legend]:text-sm",
        className
      )}
      {...props}
    />
  )
}

function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (

  )
}

const fieldVariants = cva(
  "group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
  {
    variants: {
      orientation: {
        vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
        horizontal:
          "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
        responsive:
          "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
      },
    },
    defaultVariants: {
      orientation: "vertical",
    },
  }
)

function Field({
  className,
  orientation = "vertical",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
  return (

  )
}

function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
  return (

  )
}

function FieldLabel({
  className,
  ...props
}: React.ComponentProps<typeof Label>) {
  return (
    [data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-2 dark:has-data-checked:bg-primary/10",
        "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
        className
      )}
      {...props}
    />
  )
}

function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (

  )
}

function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
  return (

a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
        className
      )}
      {...props}
    />
  )
}

function FieldSeparator({
  children,
  className,
  ...props
}: React.ComponentProps<"div"> & {
  children?: React.ReactNode
}) {
  return (

      <Separator className="absolute inset-0 top-1/2" />
      {children && (

          {children}

      )}

  )
}

function FieldError({
  className,
  children,
  errors,
  ...props
}: React.ComponentProps<"div"> & {
  errors?: Array<{ message?: string } | undefined>
}) {
  const content = useMemo(() => {
    if (children) {
      return children
    }

    if (!errors?.length) {
      return null
    }

    const uniqueErrors = [
      ...new Map(errors.map((error) => [error?.message, error])).values(),
    ]

    if (uniqueErrors?.length == 1) {
      return uniqueErrors[0]?.message
    }

    return (

        {uniqueErrors.map(
          (error, index) =>
            error?.message &&
{error.message}

        )}

    )
  }, [children, errors])

  if (!content) {
    return null
  }

  return (

      {content}

  )
}

export {
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLegend,
  FieldSeparator,
  FieldSet,
  FieldContent,
  FieldTitle,
}


# -*- coding: utf-8 -*-

"""
Office Math Markup Language (OMML)
Adapted from https://github.com/xiilei/dwml/blob/master/dwml/omml.py
On 25/03/2025
"""

from defusedxml import ElementTree as ET

from .latex_dict import (
    CHARS,
    CHR,
    CHR_BO,
    CHR_DEFAULT,
    POS,
    POS_DEFAULT,
    SUB,
    SUP,
    F,
    F_DEFAULT,
    T,
    FUNC,
    D,
    D_DEFAULT,
    RAD,
    RAD_DEFAULT,
    ARR,
    LIM_FUNC,
    LIM_TO,
    LIM_UPP,
    M,
    BRK,
    BLANK,
    BACKSLASH,
    ALN,
    FUNC_PLACE,
)

OMML_NS = "{http://schemas.openxmlformats.org/officeDocument/2006/math}"

def load(stream):
    tree = ET.parse(stream)
    for omath in tree.findall(OMML_NS + "oMath"):
        yield oMath2Latex(omath)

def load_string(string):
    root = ET.fromstring(string)
    for omath in root.findall(OMML_NS + "oMath"):
        yield oMath2Latex(omath)

def escape_latex(strs):
    last = None
    new_chr = []
    strs = strs.replace(r"\\", "\\")
    for c in strs:
        if (c in CHARS) and (last != BACKSLASH):
            new_chr.append(BACKSLASH + c)
        else:
            new_chr.append(c)
        last = c
    return BLANK.join(new_chr)

def get_val(key, default=None, store=CHR):
    if key is not None:
        return key if not store else store.get(key, key)
    else:
        return default

class Tag2Method(object):
    def call_method(self, elm, stag=None):
        getmethod = self.tag2meth.get
        if stag is None:
            stag = elm.tag.replace(OMML_NS, "")
        method = getmethod(stag)
        if method:
            return method(self, elm)
        else:
            return None

    def process_children_list(self, elm, include=None):
        """
        process children of the elm,return iterable
        """
        for _e in list(elm):
            if OMML_NS not in _e.tag:
                continue
            stag = _e.tag.replace(OMML_NS, "")
            if include and (stag not in include):
                continue
            t = self.call_method(_e, stag=stag)
            if t is None:
                t = self.process_unknow(_e, stag)
                if t is None:
                    continue
            yield (stag, t, _e)

    def process_children_dict(self, elm, include=None):
        """
        process children of the elm,return dict
        """
        latex_chars = dict()
        for stag, t, e in self.process_children_list(elm, include):
            latex_chars[stag] = t
        return latex_chars

    def process_children(self, elm, include=None):
        """
        process children of the elm,return string
        """
        return BLANK.join(
            (
                t if not isinstance(t, Tag2Method) else str(t)
                for stag, t, e in self.process_children_list(elm, include)
            )
        )

    def process_unknow(self, elm, stag):
        return None

class Pr(Tag2Method):
    text = ""

    __val_tags = ("chr", "pos", "begChr", "endChr", "type")

    __innerdict = None  # can't use the __dict__

    """ common properties of element"""

    def __init__(self, elm):
        self.__innerdict = {}
        self.text = self.process_children(elm)

    def __str__(self):
        return self.text

    def __unicode__(self):
        return self.__str__(self)

    def __getattr__(self, name):
        return self.__innerdict.get(name, None)

    def do_brk(self, elm):
        self.__innerdict["brk"] = BRK
        return BRK

    def do_common(self, elm):
        stag = elm.tag.replace(OMML_NS, "")
        if stag in self.__val_tags:
            t = elm.get("{0}val".format(OMML_NS))
            self.__innerdict[stag] = t
        return None

    tag2meth = {
        "brk": do_brk,
        "chr": do_common,
        "pos": do_common,
        "begChr": do_common,
        "endChr": do_common,
        "type": do_common,
    }

class oMath2Latex(Tag2Method):
    """
    Convert oMath element of omml to latex
    """

    _t_dict = T

    __direct_tags = ("box", "sSub", "sSup", "sSubSup", "num", "den", "deg", "e")

    def __init__(self, element):
        self._latex = self.process_children(element)

    def __str__(self):
        return self.latex

    def __unicode__(self):
        return self.__str__(self)

    def process_unknow(self, elm, stag):
        if stag in self.__direct_tags:
            return self.process_children(elm)
        elif stag[-2:] == "Pr":
            return Pr(elm)
        else:
            return None

    @property
    def latex(self):
        return self._latex

    def do_acc(self, elm):
        """
        the accent function
        """
        c_dict = self.process_children_dict(elm)
        latex_s = get_val(
            c_dict["accPr"].chr, default=CHR_DEFAULT.get("ACC_VAL"), store=CHR
        )
        return latex_s.format(c_dict["e"])

    def do_bar(self, elm):
        """
        the bar function
        """
        c_dict = self.process_children_dict(elm)
        pr = c_dict["barPr"]
        latex_s = get_val(pr.pos, default=POS_DEFAULT.get("BAR_VAL"), store=POS)
        return pr.text + latex_s.format(c_dict["e"])

    def do_d(self, elm):
        """
        the delimiter object
        """
        c_dict = self.process_children_dict(elm)
        pr = c_dict["dPr"]
        null = D_DEFAULT.get("null")
        s_val = get_val(pr.begChr, default=D_DEFAULT.get("left"), store=T)
        e_val = get_val(pr.endChr, default=D_DEFAULT.get("right"), store=T)
        return pr.text + D.format(
            left=null if not s_val else escape_latex(s_val),
            text=c_dict["e"],
            right=null if not e_val else escape_latex(e_val),
        )

    def do_spre(self, elm):
        """
        the Pre-Sub-Superscript object -- Not support yet
        """
        pass

    def do_sub(self, elm):
        text = self.process_children(elm)
        return SUB.format(text)

    def do_sup(self, elm):
        text = self.process_children(elm)
        return SUP.format(text)

    def do_f(self, elm):
        """
        the fraction object
        """
        c_dict = self.process_children_dict(elm)
        pr = c_dict["fPr"]
        latex_s = get_val(pr.type, default=F_DEFAULT, store=F)
        return pr.text + latex_s.format(num=c_dict.get("num"), den=c_dict.get("den"))

    def do_func(self, elm):
        """
        the Function-Apply object (Examples:sin cos)
        """
        c_dict = self.process_children_dict(elm)
        func_name = c_dict.get("fName")
        return func_name.replace(FUNC_PLACE, c_dict.get("e"))

    def do_fname(self, elm):
        """
        the func name
        """
        latex_chars = []
        for stag, t, e in self.process_children_list(elm):
            if stag == "r":
                if FUNC.get(t):
                    latex_chars.append(FUNC[t])
                else:
                    raise NotImplementedError("Not support func %s" % t)
            else:
                latex_chars.append(t)
        t = BLANK.join(latex_chars)
        return t if FUNC_PLACE in t else t + FUNC_PLACE  # do_func will replace this

    def do_groupchr(self, elm):
        """
        the Group-Character object
        """
        c_dict = self.process_children_dict(elm)
        pr = c_dict["groupChrPr"]
        latex_s = get_val(pr.chr)
        return pr.text + latex_s.format(c_dict["e"])

    def do_rad(self, elm):
        """
        the radical object
        """
        c_dict = self.process_children_dict(elm)
        text = c_dict.get("e")
        deg_text = c_dict.get("deg")
        if deg_text:
            return RAD.format(deg=deg_text, text=text)
        else:
            return RAD_DEFAULT.format(text=text)

    def do_eqarr(self, elm):
        """
        the Array object
        """
        return ARR.format(
            text=BRK.join(
                [t for stag, t, e in self.process_children_list(elm, include=("e",))]
            )
        )

    def do_limlow(self, elm):
        """
        the Lower-Limit object
        """
        t_dict = self.process_children_dict(elm, include=("e", "lim"))
        latex_s = LIM_FUNC.get(t_dict["e"])
        if not latex_s:
            raise NotImplementedError("Not support lim %s" % t_dict["e"])
        else:
            return latex_s.format(lim=t_dict.get("lim"))

    def do_limupp(self, elm):
        """
        the Upper-Limit object
        """
        t_dict = self.process_children_dict(elm, include=("e", "lim"))
        return LIM_UPP.format(lim=t_dict.get("lim"), text=t_dict.get("e"))

    def do_lim(self, elm):
        """
        the lower limit of the limLow object and the upper limit of the limUpp function
        """
        return self.process_children(elm).replace(LIM_TO[0], LIM_TO[1])

    def do_m(self, elm):
        """
        the Matrix object
        """
        rows = []
        for stag, t, e in self.process_children_list(elm):
            if stag == "mPr":
                pass
            elif stag == "mr":
                rows.append(t)
        return M.format(text=BRK.join(rows))

    def do_mr(self, elm):
        """
        a single row of the matrix m
        """
        return ALN.join(
            [t for stag, t, e in self.process_children_list(elm, include=("e",))]
        )

    def do_nary(self, elm):
        """
        the n-ary object
        """
        res = []
        bo = ""
        for stag, t, e in self.process_children_list(elm):
            if stag == "naryPr":
                bo = get_val(t.chr, store=CHR_BO)
            else:
                res.append(t)
        return bo + BLANK.join(res)

    def do_r(self, elm):
        """
        Get text from 'r' element,And try convert them to latex symbols
        @todo text style support , (sty)
        @todo \text (latex pure text support)
        """
        _str = []
        for s in elm.findtext("./{0}t".format(OMML_NS)):
            # s = s if isinstance(s,unicode) else unicode(s,'utf-8')
            _str.append(self._t_dict.get(s, s))
        return escape_latex(BLANK.join(_str))

    tag2meth = {
        "acc": do_acc,
        "r": do_r,
        "bar": do_bar,
        "sub": do_sub,
        "sup": do_sup,
        "f": do_f,
        "func": do_func,
        "fName": do_fname,
        "groupChr": do_groupchr,
        "d": do_d,
        "rad": do_rad,
        "eqArr": do_eqarr,
        "limLow": do_limlow,
        "limUpp": do_limupp,
        "lim": do_lim,
        "m": do_m,
        "mr": do_mr,
        "nary": do_nary,
    }