From 9ed155112516b2b2da60970303972adca3e3ac29 Mon Sep 17 00:00:00 2001 From: Aravind Karnam Date: Wed, 14 Aug 2024 10:59:49 +0530 Subject: [PATCH 01/57] Added support to source tags wrapped inside video and audio tags. Extended the text extraction to video and audio elements in media. https://github.com/unclecode/crawl4ai/issues/71 --- crawl4ai/utils.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/crawl4ai/utils.py b/crawl4ai/utils.py index 07832888..d724988b 100644 --- a/crawl4ai/utils.py +++ b/crawl4ai/utils.py @@ -452,6 +452,19 @@ def get_content_of_website_optimized(url: str, html: str, word_count_threshold: links = {'internal': [], 'external': []} media = {'images': [], 'videos': [], 'audios': []} + # Extract meaningful text for media files from closest parent + def find_closest_parent_with_useful_text(tag): + current_tag = tag + while current_tag: + current_tag = current_tag.parent + # Get the text content of the parent tag + if current_tag: + text_content = current_tag.get_text(separator=' ',strip=True) + # Check if the text content has at least word_count_threshold + if len(text_content.split()) >= image_description_min_word_threshold: + return text_content + return None + def process_image(img, url, index, total_images): #Check if an image has valid display and inside undesired html elements def is_valid_image(img, parent, parent_classes): @@ -523,19 +536,6 @@ def get_content_of_website_optimized(url: str, html: str, word_count_threshold: score+=1 return score - # Extract meaningful text for images from closest parent - def find_closest_parent_with_useful_text(tag): - current_tag = tag - while current_tag: - current_tag = current_tag.parent - # Get the text content of the parent tag - if current_tag: - text_content = current_tag.get_text(separator=' ',strip=True) - # Check if the text content has at least word_count_threshold - if len(text_content.split()) >= image_description_min_word_threshold: - return text_content - return None - if not is_valid_image(img, img.parent, img.parent.get('class', [])): return None score = score_image_for_usefulness(img, url, index, total_images) @@ -579,7 +579,16 @@ def get_content_of_website_optimized(url: str, html: str, word_count_threshold: media[f"{element.name}s"].append({ 'src': element.get('src'), 'alt': element.get('alt'), - 'type': element.name + 'type': element.name, + 'description': find_closest_parent_with_useful_text(element) + }) + source_tags = element.find_all('source') + for source_tag in source_tags: + media[f"{element.name}s"].append({ + 'src': source_tag.get('src'), + 'alt': element.get('alt'), + 'type': element.name, + 'description': find_closest_parent_with_useful_text(element) }) return True # Always keep video and audio elements From dec3d44224ff6fff5a8824fa7974db46d4c30fa2 Mon Sep 17 00:00:00 2001 From: unclecode Date: Mon, 19 Aug 2024 15:37:07 +0800 Subject: [PATCH 02/57] refactor: Update extraction strategy to handle schema extraction with non-empty schema This code change updates the `LLMExtractionStrategy` class to handle schema extraction when the schema is non-empty. Previously, the schema extraction was only triggered when the `extract_type` was set to "schema", regardless of whether a schema was provided. With this update, the schema extraction will only be performed if the `extract_type` is "schema" and a non-empty schema is provided. This ensures that the extraction strategy behaves correctly and avoids unnecessary schema extraction when not needed. Also "numpy" is removed from default installation mode. --- crawl4ai/extraction_strategy.py | 2 +- crawl4ai/utils.py | 3 --- setup.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crawl4ai/extraction_strategy.py b/crawl4ai/extraction_strategy.py index 080229f4..d04ec298 100644 --- a/crawl4ai/extraction_strategy.py +++ b/crawl4ai/extraction_strategy.py @@ -101,7 +101,7 @@ class LLMExtractionStrategy(ExtractionStrategy): variable_values["REQUEST"] = self.instruction prompt_with_variables = PROMPT_EXTRACT_BLOCKS_WITH_INSTRUCTION - if self.extract_type == "schema": + if self.extract_type == "schema" and self.schema: variable_values["SCHEMA"] = json.dumps(self.schema, indent=2) prompt_with_variables = PROMPT_EXTRACT_SCHEMA_WITH_INSTRUCTION diff --git a/crawl4ai/utils.py b/crawl4ai/utils.py index 07832888..b6f97223 100644 --- a/crawl4ai/utils.py +++ b/crawl4ai/utils.py @@ -834,7 +834,6 @@ def extract_blocks_batch(batch_data, provider = "groq/llama3-70b-8192", api_toke return sum(all_blocks, []) - def merge_chunks_based_on_token_threshold(chunks, token_threshold): """ Merges small chunks into larger ones based on the total token threshold. @@ -880,7 +879,6 @@ def process_sections(url: str, sections: list, provider: str, api_token: str) -> return extracted_content - def wrap_text(draw, text, font, max_width): # Wrap the text to fit within the specified width lines = [] @@ -892,7 +890,6 @@ def wrap_text(draw, text, font, max_width): lines.append(line) return '\n'.join(lines) - def format_html(html_string): soup = BeautifulSoup(html_string, 'html.parser') return soup.prettify() diff --git a/setup.py b/setup.py index af929125..841f85a8 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ with open("requirements.txt") as f: requirements = f.read().splitlines() # Define the requirements for different environments -default_requirements = [req for req in requirements if not req.startswith(("torch", "transformers", "onnxruntime", "nltk", "spacy", "tokenizers", "scikit-learn", "numpy"))] +default_requirements = [req for req in requirements if not req.startswith(("torch", "transformers", "onnxruntime", "nltk", "spacy", "tokenizers", "scikit-learn"))] torch_requirements = [req for req in requirements if req.startswith(("torch", "nltk", "spacy", "scikit-learn", "numpy"))] transformer_requirements = [req for req in requirements if req.startswith(("transformers", "tokenizers", "onnxruntime"))] From eba831ca30c7c3e97874c9c8a3acb1ec9df0ce95 Mon Sep 17 00:00:00 2001 From: Datehoer <62844803+datehoer@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:29:23 +0800 Subject: [PATCH 03/57] fix spelling mistake --- crawl4ai/prompts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crawl4ai/prompts.py b/crawl4ai/prompts.py index 323c4774..a55d6fca 100644 --- a/crawl4ai/prompts.py +++ b/crawl4ai/prompts.py @@ -29,7 +29,7 @@ To generate the JSON objects: 5. Make sure the generated JSON is complete and parsable, with no errors or omissions. -6. Make sur to escape any special characters in the HTML content, and also single or double quote to avoid JSON parsing issues. +6. Make sure to escape any special characters in the HTML content, and also single or double quote to avoid JSON parsing issues. Please provide your output within tags, like this: @@ -87,7 +87,7 @@ To generate the JSON objects: 5. Make sure the generated JSON is complete and parsable, with no errors or omissions. -6. Make sur to escape any special characters in the HTML content, and also single or double quote to avoid JSON parsing issues. +6. Make sure to escape any special characters in the HTML content, and also single or double quote to avoid JSON parsing issues. 7. Never alter the extracted content, just copy and paste it as it is. @@ -142,7 +142,7 @@ To generate the JSON objects: 5. Make sure the generated JSON is complete and parsable, with no errors or omissions. -6. Make sur to escape any special characters in the HTML content, and also single or double quote to avoid JSON parsing issues. +6. Make sure to escape any special characters in the HTML content, and also single or double quote to avoid JSON parsing issues. 7. Never alter the extracted content, just copy and paste it as it is. @@ -201,4 +201,4 @@ Avoid Common Mistakes: - Do not generate the Python coee show me how to do the task, this is your task to extract the information and return it in JSON format. Result -Output the final list of JSON objects, wrapped in ... XML tags. Make sure to close the tag properly.""" \ No newline at end of file +Output the final list of JSON objects, wrapped in ... XML tags. Make sure to close the tag properly.""" From fe9ff498ce1cbb3f453473c1721dfd306e60f3ee Mon Sep 17 00:00:00 2001 From: datehoer Date: Mon, 26 Aug 2024 16:12:49 +0800 Subject: [PATCH 04/57] add proxy and add ai base_url --- crawl4ai/crawler_strategy.py | 2 ++ crawl4ai/extraction_strategy.py | 3 ++- crawl4ai/utils.py | 13 +++++++------ crawl4ai/web_crawler.py | 3 ++- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crawl4ai/crawler_strategy.py b/crawl4ai/crawler_strategy.py index fb7980d3..66a8f7dd 100644 --- a/crawl4ai/crawler_strategy.py +++ b/crawl4ai/crawler_strategy.py @@ -82,6 +82,8 @@ class LocalSeleniumCrawlerStrategy(CrawlerStrategy): print("[LOG] 🚀 Initializing LocalSeleniumCrawlerStrategy") self.options = Options() self.options.headless = True + if kwargs.get("proxy"): + self.options.add_argument("--proxy-server={}".format(kwargs.get("proxy"))) if kwargs.get("user_agent"): self.options.add_argument("--user-agent=" + kwargs.get("user_agent")) else: diff --git a/crawl4ai/extraction_strategy.py b/crawl4ai/extraction_strategy.py index 080229f4..8096f11c 100644 --- a/crawl4ai/extraction_strategy.py +++ b/crawl4ai/extraction_strategy.py @@ -79,6 +79,7 @@ class LLMExtractionStrategy(ExtractionStrategy): self.overlap_rate = kwargs.get("overlap_rate", OVERLAP_RATE) self.word_token_rate = kwargs.get("word_token_rate", WORD_TOKEN_RATE) self.apply_chunking = kwargs.get("apply_chunking", True) + self.base_url = kwargs.get("base_url", None) if not self.apply_chunking: self.chunk_token_threshold = 1e9 @@ -110,7 +111,7 @@ class LLMExtractionStrategy(ExtractionStrategy): "{" + variable + "}", variable_values[variable] ) - response = perform_completion_with_backoff(self.provider, prompt_with_variables, self.api_token) # , json_response=self.extract_type == "schema") + response = perform_completion_with_backoff(self.provider, prompt_with_variables, self.api_token, base_url=self.base_url) # , json_response=self.extract_type == "schema") try: blocks = extract_xml_data(["blocks"], response.choices[0].message.content)['blocks'] blocks = json.loads(blocks) diff --git a/crawl4ai/utils.py b/crawl4ai/utils.py index 07832888..64ce9f57 100644 --- a/crawl4ai/utils.py +++ b/crawl4ai/utils.py @@ -716,7 +716,7 @@ def extract_xml_data(tags, string): return data # Function to perform the completion with exponential backoff -def perform_completion_with_backoff(provider, prompt_with_variables, api_token, json_response = False): +def perform_completion_with_backoff(provider, prompt_with_variables, api_token, json_response = False, base_url=None): from litellm import completion from litellm.exceptions import RateLimitError max_attempts = 3 @@ -735,6 +735,7 @@ def perform_completion_with_backoff(provider, prompt_with_variables, api_token, ], temperature=0.01, api_key=api_token, + base_url=base_url, **extra_args ) return response # Return the successful response @@ -755,7 +756,7 @@ def perform_completion_with_backoff(provider, prompt_with_variables, api_token, "content": ["Rate limit error. Please try again later."] }] -def extract_blocks(url, html, provider = DEFAULT_PROVIDER, api_token = None): +def extract_blocks(url, html, provider = DEFAULT_PROVIDER, api_token = None, base_url = None): # api_token = os.getenv('GROQ_API_KEY', None) if not api_token else api_token api_token = PROVIDER_MODELS.get(provider, None) if not api_token else api_token @@ -770,7 +771,7 @@ def extract_blocks(url, html, provider = DEFAULT_PROVIDER, api_token = None): "{" + variable + "}", variable_values[variable] ) - response = perform_completion_with_backoff(provider, prompt_with_variables, api_token) + response = perform_completion_with_backoff(provider, prompt_with_variables, api_token, base_url=base_url) try: blocks = extract_xml_data(["blocks"], response.choices[0].message.content)['blocks'] @@ -864,17 +865,17 @@ def merge_chunks_based_on_token_threshold(chunks, token_threshold): return merged_sections -def process_sections(url: str, sections: list, provider: str, api_token: str) -> list: +def process_sections(url: str, sections: list, provider: str, api_token: str, base_url=None) -> list: extracted_content = [] if provider.startswith("groq/"): # Sequential processing with a delay for section in sections: - extracted_content.extend(extract_blocks(url, section, provider, api_token)) + extracted_content.extend(extract_blocks(url, section, provider, api_token, base_url=base_url)) time.sleep(0.5) # 500 ms delay between each processing else: # Parallel processing using ThreadPoolExecutor with ThreadPoolExecutor() as executor: - futures = [executor.submit(extract_blocks, url, section, provider, api_token) for section in sections] + futures = [executor.submit(extract_blocks, url, section, provider, api_token, base_url=base_url) for section in sections] for future in as_completed(futures): extracted_content.extend(future.result()) diff --git a/crawl4ai/web_crawler.py b/crawl4ai/web_crawler.py index db0d9856..b354b5cd 100644 --- a/crawl4ai/web_crawler.py +++ b/crawl4ai/web_crawler.py @@ -22,9 +22,10 @@ class WebCrawler: crawler_strategy: CrawlerStrategy = None, always_by_pass_cache: bool = False, verbose: bool = False, + proxy: str = None, ): # self.db_path = db_path - self.crawler_strategy = crawler_strategy or LocalSeleniumCrawlerStrategy(verbose=verbose) + self.crawler_strategy = crawler_strategy or LocalSeleniumCrawlerStrategy(verbose=verbose, proxy=proxy) self.always_by_pass_cache = always_by_pass_cache # Create the .crawl4ai folder in the user's home directory if it doesn't exist From 16f98cebc0b0e75c0842aa4d13e45cbaea9ec8af Mon Sep 17 00:00:00 2001 From: datehoer Date: Tue, 27 Aug 2024 09:44:35 +0800 Subject: [PATCH 05/57] replace base64 image url to '' --- crawl4ai/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crawl4ai/utils.py b/crawl4ai/utils.py index 64ce9f57..2ea6fec7 100644 --- a/crawl4ai/utils.py +++ b/crawl4ai/utils.py @@ -634,7 +634,12 @@ def get_content_of_website_optimized(url: str, html: str, word_count_threshold: return node body = flatten_nested_elements(body) - + base64_pattern = re.compile(r'data:image/[^;]+;base64,([^"]+)') + for img in imgs: + src = img.get('src', '') + if base64_pattern.match(src): + # Replace base64 data with empty string + img['src'] = base64_pattern.sub('', src) cleaned_html = str(body).replace('\n\n', '\n').replace(' ', ' ') cleaned_html = sanitize_html(cleaned_html) From 2ba70b95018756077cb26ab90a76f6cc019aa7b3 Mon Sep 17 00:00:00 2001 From: datehoer Date: Tue, 27 Aug 2024 10:14:54 +0800 Subject: [PATCH 06/57] add use proxy and llm baseurl examples --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index f2975ad7..6bbef7e4 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,33 @@ result = crawler.run( print(result.extracted_content) ``` +### Extract Structured Data from Web Pages With Proxy and BaseUrl + +```python +from crawl4ai import WebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +def create_crawler(): + crawler = WebCrawler(verbose=True, proxy="http://127.0.0.1:7890") + crawler.warmup() + return crawler + +crawler = create_crawler() + +crawler.warmup() + +result = crawler.run( + url="https://www.nbcnews.com/business", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token="sk-", + base_url="https://api.openai.com/v1" + ) +) + +print(result.markdown) +``` + ## Documentation 📚 For detailed documentation, including installation instructions, advanced features, and API reference, visit our [Documentation Website](https://crawl4ai.com/mkdocs/). From 3c6ebb73ae93484aa70218de29b49e41abffec68 Mon Sep 17 00:00:00 2001 From: Umut CAN <78921017+C1N-S4@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:30:06 +0300 Subject: [PATCH 07/57] Update web_crawler.py Improve code efficiency, readability, and maintainability in web_crawler.py --- crawl4ai/web_crawler.py | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/crawl4ai/web_crawler.py b/crawl4ai/web_crawler.py index db0d9856..351fa62e 100644 --- a/crawl4ai/web_crawler.py +++ b/crawl4ai/web_crawler.py @@ -16,40 +16,23 @@ warnings.filterwarnings("ignore", message='Field "model_name" has conflict with class WebCrawler: - def __init__( - self, - # db_path: str = None, - crawler_strategy: CrawlerStrategy = None, - always_by_pass_cache: bool = False, - verbose: bool = False, - ): - # self.db_path = db_path + def __init__(self, crawler_strategy: CrawlerStrategy = None, always_by_pass_cache: bool = False, verbose: bool = False): self.crawler_strategy = crawler_strategy or LocalSeleniumCrawlerStrategy(verbose=verbose) self.always_by_pass_cache = always_by_pass_cache - - # Create the .crawl4ai folder in the user's home directory if it doesn't exist self.crawl4ai_folder = os.path.join(Path.home(), ".crawl4ai") os.makedirs(self.crawl4ai_folder, exist_ok=True) os.makedirs(f"{self.crawl4ai_folder}/cache", exist_ok=True) - - # If db_path is not provided, use the default path - # if not db_path: - # self.db_path = f"{self.crawl4ai_folder}/crawl4ai.db" - - # flush_db() init_db() - self.ready = False def warmup(self): print("[LOG] 🌤️ Warming up the WebCrawler") - result = self.run( + self.run( url='https://google.com/', word_count_threshold=5, - extraction_strategy= NoExtractionStrategy(), + extraction_strategy=NoExtractionStrategy(), bypass_cache=False, - verbose = False, - # warmup=True + verbose=False ) self.ready = True print("[LOG] 🌞 WebCrawler is ready to crawl") @@ -139,12 +122,8 @@ class WebCrawler: if not isinstance(chunking_strategy, ChunkingStrategy): raise ValueError("Unsupported chunking strategy") - # if word_count_threshold < MIN_WORD_THRESHOLD: - # word_count_threshold = MIN_WORD_THRESHOLD - word_count_threshold = max(word_count_threshold, 0) - # Check cache first cached = None screenshot_data = None extracted_content = None @@ -169,7 +148,7 @@ class WebCrawler: html = sanitize_input_encode(self.crawler_strategy.crawl(url, **kwargs)) t2 = time.time() if verbose: - print(f"[LOG] 🚀 Crawling done for {url}, success: {bool(html)}, time taken: {t2 - t1} seconds") + print(f"[LOG] 🚀 Crawling done for {url}, success: {bool(html)}, time taken: {t2 - t1:.2f} seconds") if screenshot: screenshot_data = self.crawler_strategy.take_screenshot() @@ -200,13 +179,10 @@ class WebCrawler: t = time.time() # Extract content from HTML try: - # t1 = time.time() - # result = get_content_of_website(url, html, word_count_threshold, css_selector=css_selector, only_text=kwargs.get("only_text", False)) - # print(f"[LOG] 🚀 Crawling done for {url}, success: True, time taken: {time.time() - t1} seconds") t1 = time.time() result = get_content_of_website_optimized(url, html, word_count_threshold, css_selector=css_selector, only_text=kwargs.get("only_text", False)) if verbose: - print(f"[LOG] 🚀 Content extracted for {url}, success: True, time taken: {time.time() - t1} seconds") + print(f"[LOG] 🚀 Content extracted for {url}, success: True, time taken: {time.time() - t1:.2f} seconds") if result is None: raise ValueError(f"Failed to extract content from the website: {url}") @@ -228,7 +204,7 @@ class WebCrawler: extracted_content = json.dumps(extracted_content, indent=4, default=str) if verbose: - print(f"[LOG] 🚀 Extraction done for {url}, time taken: {time.time() - t} seconds.") + print(f"[LOG] 🚀 Extraction done for {url}, time taken: {time.time() - t:.2f} seconds.") screenshot = None if not screenshot else screenshot From 3caf48c9be3e289e8e918c185486969f011ee9a5 Mon Sep 17 00:00:00 2001 From: unclecode Date: Sun, 1 Sep 2024 16:34:51 +0800 Subject: [PATCH 08/57] refactor: Update LocalSeleniumCrawlerStrategy to execute JS code if provided --- crawl4ai/crawler_strategy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/crawl4ai/crawler_strategy.py b/crawl4ai/crawler_strategy.py index 66a8f7dd..4d049069 100644 --- a/crawl4ai/crawler_strategy.py +++ b/crawl4ai/crawler_strategy.py @@ -244,6 +244,7 @@ class LocalSeleniumCrawlerStrategy(CrawlerStrategy): driver.quit() # Execute JS code if provided + self.js_code = kwargs.get("js_code", self.js_code) if self.js_code and type(self.js_code) == str: self.driver.execute_script(self.js_code) # Optionally, wait for some condition after executing the JS code From c37614cbc80f5aaddacc3b4268f2138ff1f54912 Mon Sep 17 00:00:00 2001 From: unclecode Date: Tue, 3 Sep 2024 01:27:00 +0800 Subject: [PATCH 09/57] Add Async Version, JsonCss Extrator --- crawl4ai/async_crawler_strategy.py | 254 ++++++++++++++++ crawl4ai/async_database.py | 97 ++++++ crawl4ai/async_webcrawler.py | 269 +++++++++++++++++ crawl4ai/content_scrapping_strategy.py | 283 ++++++++++++++++++ crawl4ai/extraction_strategy.py | 155 ++++++++++ crawl4ai/models.py | 3 +- crawl4ai/web_crawler.py | 2 +- tests/async/test_basic_crawling.py | 81 +++++ tests/async/test_caching.py | 82 +++++ ...test_chunking_and_extraction_strategies.py | 87 ++++++ tests/async/test_content_extraction.py | 90 ++++++ tests/async/test_crawler_strategy.py | 68 +++++ tests/async/test_database_operations.py | 82 +++++ tests/async/test_edge_cases.py | 127 ++++++++ tests/async/test_error_handling.py | 78 +++++ tests/async/test_parameters_and_options.py | 94 ++++++ tests/async/test_performance.py | 72 +++++ 17 files changed, 1922 insertions(+), 2 deletions(-) create mode 100644 crawl4ai/async_crawler_strategy.py create mode 100644 crawl4ai/async_database.py create mode 100644 crawl4ai/async_webcrawler.py create mode 100644 crawl4ai/content_scrapping_strategy.py create mode 100644 tests/async/test_basic_crawling.py create mode 100644 tests/async/test_caching.py create mode 100644 tests/async/test_chunking_and_extraction_strategies.py create mode 100644 tests/async/test_content_extraction.py create mode 100644 tests/async/test_crawler_strategy.py create mode 100644 tests/async/test_database_operations.py create mode 100644 tests/async/test_edge_cases.py create mode 100644 tests/async/test_error_handling.py create mode 100644 tests/async/test_parameters_and_options.py create mode 100644 tests/async/test_performance.py diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py new file mode 100644 index 00000000..3840260e --- /dev/null +++ b/crawl4ai/async_crawler_strategy.py @@ -0,0 +1,254 @@ +import asyncio +import base64, time +from abc import ABC, abstractmethod +from typing import Callable, Dict, Any, List, Optional +import os +import psutil +from playwright.async_api import async_playwright, Page, Browser, Error +from io import BytesIO +from PIL import Image, ImageDraw, ImageFont +from .utils import sanitize_input_encode +import json, uuid +import hashlib +from pathlib import Path +from playwright.async_api import ProxySettings + +def calculate_semaphore_count(): + cpu_count = os.cpu_count() + memory_gb = psutil.virtual_memory().total / (1024 ** 3) # Convert to GB + base_count = max(1, cpu_count // 2) + memory_based_cap = int(memory_gb / 2) # Assume 2GB per instance + return min(base_count, memory_based_cap) + +class AsyncCrawlerStrategy(ABC): + @abstractmethod + async def crawl(self, url: str, **kwargs) -> str: + pass + + @abstractmethod + async def crawl_many(self, urls: List[str], **kwargs) -> List[str]: + pass + + @abstractmethod + async def take_screenshot(self, url: str) -> str: + pass + + @abstractmethod + def update_user_agent(self, user_agent: str): + pass + + @abstractmethod + def set_hook(self, hook_type: str, hook: Callable): + pass + +class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): + def __init__(self, use_cached_html=False, js_code=None, **kwargs): + self.use_cached_html = use_cached_html + self.user_agent = kwargs.get("user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + self.proxy = kwargs.get("proxy") + self.headers = {} + self.sessions = {} + self.session_ttl = 1800 + self.js_code = js_code + self.verbose = kwargs.get("verbose", False) + self.playwright = None + self.browser = None + self.hooks = { + 'on_browser_created': None, + 'on_user_agent_updated': None, + 'on_execution_started': None, + 'before_goto': None, + 'after_goto': None, + 'before_return_html': None + } + + async def __aenter__(self): + await self.start() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + + async def start(self): + if self.playwright is None: + self.playwright = await async_playwright().start() + if self.browser is None: + browser_args = { + "headless": True, + # "headless": False, + "args": [ + "--disable-gpu", + "--disable-dev-shm-usage", + "--disable-setuid-sandbox", + "--no-sandbox", + ] + } + + # Add proxy settings if a proxy is specified + if self.proxy: + proxy_settings = ProxySettings(server=self.proxy) + browser_args["proxy"] = proxy_settings + + + self.browser = await self.playwright.chromium.launch(**browser_args) + await self.execute_hook('on_browser_created', self.browser) + + async def close(self): + if self.browser: + await self.browser.close() + self.browser = None + if self.playwright: + await self.playwright.stop() + self.playwright = None + + def __del__(self): + if self.browser or self.playwright: + asyncio.get_event_loop().run_until_complete(self.close()) + + def set_hook(self, hook_type: str, hook: Callable): + if hook_type in self.hooks: + self.hooks[hook_type] = hook + else: + raise ValueError(f"Invalid hook type: {hook_type}") + + async def execute_hook(self, hook_type: str, *args): + hook = self.hooks.get(hook_type) + if hook: + if asyncio.iscoroutinefunction(hook): + return await hook(*args) + else: + return hook(*args) + return args[0] if args else None + + def update_user_agent(self, user_agent: str): + self.user_agent = user_agent + + def set_custom_headers(self, headers: Dict[str, str]): + self.headers = headers + + async def kill_session(self, session_id: str): + if session_id in self.sessions: + context, page, _ = self.sessions[session_id] + await page.close() + await context.close() + del self.sessions[session_id] + + def _cleanup_expired_sessions(self): + current_time = time.time() + expired_sessions = [sid for sid, (_, _, last_used) in self.sessions.items() + if current_time - last_used > self.session_ttl] + for sid in expired_sessions: + asyncio.create_task(self.kill_session(sid)) + + async def crawl(self, url: str, **kwargs) -> str: + self._cleanup_expired_sessions() + session_id = kwargs.get("session_id") + if session_id: + context, page, _ = self.sessions.get(session_id, (None, None, None)) + if not context: + context = await self.browser.new_context( + user_agent=self.user_agent, + proxy={"server": self.proxy} if self.proxy else None + ) + await context.set_extra_http_headers(self.headers) + page = await context.new_page() + self.sessions[session_id] = (context, page, time.time()) + else: + context = await self.browser.new_context( + user_agent=self.user_agent, + proxy={"server": self.proxy} if self.proxy else None + ) + await context.set_extra_http_headers(self.headers) + page = await context.new_page() + + try: + if self.verbose: + print(f"[LOG] 🕸️ Crawling {url} using AsyncPlaywrightCrawlerStrategy...") + + if self.use_cached_html: + cache_file_path = os.path.join(Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest()) + if os.path.exists(cache_file_path): + with open(cache_file_path, "r") as f: + return f.read() + + if not kwargs.get("js_only", False): + await self.execute_hook('before_goto', page) + await page.goto(url, wait_until="domcontentloaded", timeout=60000) + await self.execute_hook('after_goto', page) + + await page.wait_for_selector('body') + await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") + + js_code = kwargs.get("js_code", kwargs.get("js", self.js_code)) + if js_code: + if isinstance(js_code, str): + await page.evaluate(js_code) + elif isinstance(js_code, list): + for js in js_code: + await page.evaluate(js) + + # await page.wait_for_timeout(100) + await page.wait_for_load_state('networkidle') + # Check for on execution even + await self.execute_hook('on_execution_started', page) + + html = await page.content() + page = await self.execute_hook('before_return_html', page, html) + + if self.verbose: + print(f"[LOG] ✅ Crawled {url} successfully!") + + if self.use_cached_html: + cache_file_path = os.path.join(Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest()) + with open(cache_file_path, "w", encoding="utf-8") as f: + f.write(html) + + return html + except Error as e: + raise Error(f"Failed to crawl {url}: {str(e)}") + finally: + if not session_id: + await page.close() + + # try: + # html = await _crawl() + # return sanitize_input_encode(html) + # except Error as e: + # raise Error(f"Failed to crawl {url}: {str(e)}") + # except Exception as e: + # raise Exception(f"Failed to crawl {url}: {str(e)}") + + async def crawl_many(self, urls: List[str], **kwargs) -> List[str]: + semaphore_count = kwargs.get('semaphore_count', calculate_semaphore_count()) + semaphore = asyncio.Semaphore(semaphore_count) + + async def crawl_with_semaphore(url): + async with semaphore: + return await self.crawl(url, **kwargs) + + tasks = [crawl_with_semaphore(url) for url in urls] + results = await asyncio.gather(*tasks, return_exceptions=True) + return [result if not isinstance(result, Exception) else str(result) for result in results] + + async def take_screenshot(self, url: str) -> str: + async with await self.browser.new_context(user_agent=self.user_agent) as context: + page = await context.new_page() + try: + await page.goto(url, wait_until="domcontentloaded") + screenshot = await page.screenshot(full_page=True) + return base64.b64encode(screenshot).decode('utf-8') + except Exception as e: + error_message = f"Failed to take screenshot: {str(e)}" + print(error_message) + + # Generate an error image + img = Image.new('RGB', (800, 600), color='black') + draw = ImageDraw.Draw(img) + font = ImageFont.load_default() + draw.text((10, 10), error_message, fill=(255, 255, 255), font=font) + + buffered = BytesIO() + img.save(buffered, format="JPEG") + return base64.b64encode(buffered.getvalue()).decode('utf-8') + finally: + await page.close() \ No newline at end of file diff --git a/crawl4ai/async_database.py b/crawl4ai/async_database.py new file mode 100644 index 00000000..baa53255 --- /dev/null +++ b/crawl4ai/async_database.py @@ -0,0 +1,97 @@ +import os +from pathlib import Path +import aiosqlite +import asyncio +from typing import Optional, Tuple + +DB_PATH = os.path.join(Path.home(), ".crawl4ai") +os.makedirs(DB_PATH, exist_ok=True) +DB_PATH = os.path.join(DB_PATH, "crawl4ai.db") + +class AsyncDatabaseManager: + def __init__(self): + self.db_path = DB_PATH + + async def ainit_db(self): + async with aiosqlite.connect(self.db_path) as db: + await db.execute(''' + CREATE TABLE IF NOT EXISTS crawled_data ( + url TEXT PRIMARY KEY, + html TEXT, + cleaned_html TEXT, + markdown TEXT, + extracted_content TEXT, + success BOOLEAN, + media TEXT DEFAULT "{}", + links TEXT DEFAULT "{}", + metadata TEXT DEFAULT "{}", + screenshot TEXT DEFAULT "" + ) + ''') + await db.commit() + + async def aalter_db_add_screenshot(self, new_column: str = "media"): + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute(f'ALTER TABLE crawled_data ADD COLUMN {new_column} TEXT DEFAULT ""') + await db.commit() + except Exception as e: + print(f"Error altering database to add screenshot column: {e}") + + async def aget_cached_url(self, url: str) -> Optional[Tuple[str, str, str, str, str, str, str, bool, str]]: + try: + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT url, html, cleaned_html, markdown, extracted_content, success, media, links, metadata, screenshot FROM crawled_data WHERE url = ?', (url,)) as cursor: + return await cursor.fetchone() + except Exception as e: + print(f"Error retrieving cached URL: {e}") + return None + + async def acache_url(self, url: str, html: str, cleaned_html: str, markdown: str, extracted_content: str, success: bool, media: str = "{}", links: str = "{}", metadata: str = "{}", screenshot: str = ""): + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute(''' + INSERT INTO crawled_data (url, html, cleaned_html, markdown, extracted_content, success, media, links, metadata, screenshot) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(url) DO UPDATE SET + html = excluded.html, + cleaned_html = excluded.cleaned_html, + markdown = excluded.markdown, + extracted_content = excluded.extracted_content, + success = excluded.success, + media = excluded.media, + links = excluded.links, + metadata = excluded.metadata, + screenshot = excluded.screenshot + ''', (url, html, cleaned_html, markdown, extracted_content, success, media, links, metadata, screenshot)) + await db.commit() + except Exception as e: + print(f"Error caching URL: {e}") + + async def aget_total_count(self) -> int: + try: + async with aiosqlite.connect(self.db_path) as db: + async with db.execute('SELECT COUNT(*) FROM crawled_data') as cursor: + result = await cursor.fetchone() + return result[0] if result else 0 + except Exception as e: + print(f"Error getting total count: {e}") + return 0 + + async def aclear_db(self): + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute('DELETE FROM crawled_data') + await db.commit() + except Exception as e: + print(f"Error clearing database: {e}") + + async def aflush_db(self): + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute('DROP TABLE IF EXISTS crawled_data') + await db.commit() + except Exception as e: + print(f"Error flushing database: {e}") + +async_db_manager = AsyncDatabaseManager() \ No newline at end of file diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py new file mode 100644 index 00000000..212d59ca --- /dev/null +++ b/crawl4ai/async_webcrawler.py @@ -0,0 +1,269 @@ +import os +import time +from pathlib import Path +from typing import Optional +import json +import asyncio +from .models import CrawlResult +from .async_database import async_db_manager +from .chunking_strategy import * +from .extraction_strategy import * +from .async_crawler_strategy import AsyncCrawlerStrategy, AsyncPlaywrightCrawlerStrategy +from .content_scrapping_strategy import WebScrappingStrategy +from .config import MIN_WORD_THRESHOLD, IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD +from .utils import ( + sanitize_input_encode, + InvalidCSSSelectorError, + format_html +) + + +class AsyncWebCrawler: + def __init__( + self, + crawler_strategy: Optional[AsyncCrawlerStrategy] = None, + always_by_pass_cache: bool = False, + verbose: bool = False, + ): + self.crawler_strategy = crawler_strategy or AsyncPlaywrightCrawlerStrategy( + verbose=verbose + ) + self.always_by_pass_cache = always_by_pass_cache + self.crawl4ai_folder = os.path.join(Path.home(), ".crawl4ai") + os.makedirs(self.crawl4ai_folder, exist_ok=True) + os.makedirs(f"{self.crawl4ai_folder}/cache", exist_ok=True) + self.ready = False + self.verbose = verbose + + async def __aenter__(self): + await self.crawler_strategy.__aenter__() + await self.awarmup() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.crawler_strategy.__aexit__(exc_type, exc_val, exc_tb) + + async def awarmup(self): + print("[LOG] 🌤️ Warming up the AsyncWebCrawler") + await async_db_manager.ainit_db() + await self.arun( + url="https://google.com/", + word_count_threshold=5, + bypass_cache=False, + verbose=False, + ) + self.ready = True + print("[LOG] 🌞 AsyncWebCrawler is ready to crawl") + + async def arun( + self, + url: str, + word_count_threshold=MIN_WORD_THRESHOLD, + extraction_strategy: ExtractionStrategy = None, + chunking_strategy: ChunkingStrategy = RegexChunking(), + bypass_cache: bool = False, + css_selector: str = None, + screenshot: bool = False, + user_agent: str = None, + verbose=True, + **kwargs, + ) -> CrawlResult: + try: + extraction_strategy = extraction_strategy or NoExtractionStrategy() + extraction_strategy.verbose = verbose + if not isinstance(extraction_strategy, ExtractionStrategy): + raise ValueError("Unsupported extraction strategy") + if not isinstance(chunking_strategy, ChunkingStrategy): + raise ValueError("Unsupported chunking strategy") + + word_count_threshold = max(word_count_threshold, MIN_WORD_THRESHOLD) + + cached = None + screenshot_data = None + extracted_content = None + if not bypass_cache and not self.always_by_pass_cache: + cached = await async_db_manager.aget_cached_url(url) + + if kwargs.get("warmup", True) and not self.ready: + return None + + if cached: + html = sanitize_input_encode(cached[1]) + extracted_content = sanitize_input_encode(cached[4]) + if screenshot: + screenshot_data = cached[9] + if not screenshot_data: + cached = None + + if not cached or not html: + t1 = time.time() + if user_agent: + self.crawler_strategy.update_user_agent(user_agent) + html = await self.crawler_strategy.crawl(url, **kwargs) + t2 = time.time() + if verbose: + print( + f"[LOG] 🚀 Crawling done for {url}, success: {bool(html)}, time taken: {t2 - t1:.2f} seconds" + ) + if screenshot: + screenshot_data = await self.crawler_strategy.take_screenshot(url) + + crawl_result = await self.aprocess_html( + url, + html, + extracted_content, + word_count_threshold, + extraction_strategy, + chunking_strategy, + css_selector, + screenshot_data, + verbose, + bool(cached), + **kwargs, + ) + crawl_result.success = bool(html) + crawl_result.session_id = kwargs.get("session_id", None) + return crawl_result + except Exception as e: + if not hasattr(e, "msg"): + e.msg = str(e) + print(f"[ERROR] 🚫 Failed to crawl {url}, error: {e.msg}") + return CrawlResult(url=url, html="", success=False, error_message=e.msg) + + async def arun_many( + self, + urls: List[str], + word_count_threshold=MIN_WORD_THRESHOLD, + extraction_strategy: ExtractionStrategy = None, + chunking_strategy: ChunkingStrategy = RegexChunking(), + bypass_cache: bool = False, + css_selector: str = None, + screenshot: bool = False, + user_agent: str = None, + verbose=True, + **kwargs, + ) -> List[CrawlResult]: + tasks = [ + self.arun( + url, + word_count_threshold, + extraction_strategy, + chunking_strategy, + bypass_cache, + css_selector, + screenshot, + user_agent, + verbose, + **kwargs + ) + for url in urls + ] + return await asyncio.gather(*tasks) + + + async def aprocess_html( + self, + url: str, + html: str, + extracted_content: str, + word_count_threshold: int, + extraction_strategy: ExtractionStrategy, + chunking_strategy: ChunkingStrategy, + css_selector: str, + screenshot: str, + verbose: bool, + is_cached: bool, + **kwargs, + ) -> CrawlResult: + t = time.time() + # Extract content from HTML + try: + t1 = time.time() + scrapping_strategy = WebScrappingStrategy() + result = await scrapping_strategy.ascrap( + url, + html, + word_count_threshold=word_count_threshold, + css_selector=css_selector, + only_text=kwargs.get("only_text", False), + image_description_min_word_threshold=kwargs.get( + "image_description_min_word_threshold", IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD + ), + ) + if verbose: + print( + f"[LOG] 🚀 Content extracted for {url}, success: True, time taken: {time.time() - t1:.2f} seconds" + ) + + if result is None: + raise ValueError(f"Failed to extract content from the website: {url}") + except InvalidCSSSelectorError as e: + raise ValueError(str(e)) + except Exception as e: + raise ValueError(f"Failed to extract content from the website: {url}, error: {str(e)}") + + cleaned_html = sanitize_input_encode(result.get("cleaned_html", "")) + markdown = sanitize_input_encode(result.get("markdown", "")) + media = result.get("media", []) + links = result.get("links", []) + metadata = result.get("metadata", {}) + + if extracted_content is None and extraction_strategy and chunking_strategy: + if verbose: + print( + f"[LOG] 🔥 Extracting semantic blocks for {url}, Strategy: {self.__class__.__name__}" + ) + + # Check if extraction strategy is type of JsonCssExtractionStrategy + if isinstance(extraction_strategy, JsonCssExtractionStrategy) or isinstance(extraction_strategy, EnhancedJsonCssExtractionStrategy): + extraction_strategy.verbose = verbose + extracted_content = extraction_strategy.run(url, [html]) + extracted_content = json.dumps(extracted_content, indent=4, default=str) + else: + sections = chunking_strategy.chunk(markdown) + extracted_content = extraction_strategy.run(url, sections) + extracted_content = json.dumps(extracted_content, indent=4, default=str) + + if verbose: + print( + f"[LOG] 🚀 Extraction done for {url}, time taken: {time.time() - t:.2f} seconds." + ) + + screenshot = None if not screenshot else screenshot + + if not is_cached: + await async_db_manager.acache_url( + url, + html, + cleaned_html, + markdown, + extracted_content, + True, + json.dumps(media), + json.dumps(links), + json.dumps(metadata), + screenshot=screenshot, + ) + + return CrawlResult( + url=url, + html=html, + cleaned_html=format_html(cleaned_html), + markdown=markdown, + media=media, + links=links, + metadata=metadata, + screenshot=screenshot, + extracted_content=extracted_content, + success=True, + error_message="", + ) + + async def aclear_cache(self): + await async_db_manager.aclear_db() + + async def aflush_cache(self): + await async_db_manager.aflush_db() + + async def aget_cache_size(self): + return await async_db_manager.aget_total_count() diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py new file mode 100644 index 00000000..56868354 --- /dev/null +++ b/crawl4ai/content_scrapping_strategy.py @@ -0,0 +1,283 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any +from bs4 import BeautifulSoup +from concurrent.futures import ThreadPoolExecutor +import asyncio, requests, re, os +from .config import * +from bs4 import element, NavigableString, Comment +from urllib.parse import urljoin +from requests.exceptions import InvalidSchema + +from .utils import ( + sanitize_input_encode, + sanitize_html, + extract_metadata, + InvalidCSSSelectorError, + CustomHTML2Text +) + + + +class ContentScrappingStrategy(ABC): + @abstractmethod + def scrap(self, url: str, html: str, **kwargs) -> Dict[str, Any]: + pass + + @abstractmethod + async def ascrap(self, url: str, html: str, **kwargs) -> Dict[str, Any]: + pass + +class WebScrappingStrategy(ContentScrappingStrategy): + def scrap(self, url: str, html: str, **kwargs) -> Dict[str, Any]: + return self._get_content_of_website_optimized(url, html, is_async=False, **kwargs) + + async def ascrap(self, url: str, html: str, **kwargs) -> Dict[str, Any]: + return await asyncio.to_thread(self._get_content_of_website_optimized, url, html, **kwargs) + + def _get_content_of_website_optimized(self, url: str, html: str, word_count_threshold: int = MIN_WORD_THRESHOLD, css_selector: str = None, **kwargs) -> Dict[str, Any]: + if not html: + return None + + soup = BeautifulSoup(html, 'html.parser') + body = soup.body + + image_description_min_word_threshold = kwargs.get('image_description_min_word_threshold', IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD) + + if css_selector: + selected_elements = body.select(css_selector) + if not selected_elements: + raise InvalidCSSSelectorError(f"Invalid CSS selector, No elements found for CSS selector: {css_selector}") + body = soup.new_tag('div') + for el in selected_elements: + body.append(el) + + links = {'internal': [], 'external': []} + media = {'images': [], 'videos': [], 'audios': []} + + # Extract meaningful text for media files from closest parent + def find_closest_parent_with_useful_text(tag): + current_tag = tag + while current_tag: + current_tag = current_tag.parent + # Get the text content of the parent tag + if current_tag: + text_content = current_tag.get_text(separator=' ',strip=True) + # Check if the text content has at least word_count_threshold + if len(text_content.split()) >= image_description_min_word_threshold: + return text_content + return None + + def process_image(img, url, index, total_images): + #Check if an image has valid display and inside undesired html elements + def is_valid_image(img, parent, parent_classes): + style = img.get('style', '') + src = img.get('src', '') + classes_to_check = ['button', 'icon', 'logo'] + tags_to_check = ['button', 'input'] + return all([ + 'display:none' not in style, + src, + not any(s in var for var in [src, img.get('alt', ''), *parent_classes] for s in classes_to_check), + parent.name not in tags_to_check + ]) + + #Score an image for it's usefulness + def score_image_for_usefulness(img, base_url, index, images_count): + # Function to parse image height/width value and units + def parse_dimension(dimension): + if dimension: + match = re.match(r"(\d+)(\D*)", dimension) + if match: + number = int(match.group(1)) + unit = match.group(2) or 'px' # Default unit is 'px' if not specified + return number, unit + return None, None + + # Fetch image file metadata to extract size and extension + def fetch_image_file_size(img, base_url): + #If src is relative path construct full URL, if not it may be CDN URL + img_url = urljoin(base_url,img.get('src')) + try: + response = requests.head(img_url) + if response.status_code == 200: + return response.headers.get('Content-Length',None) + else: + print(f"Failed to retrieve file size for {img_url}") + return None + except InvalidSchema as e: + return None + finally: + return + + image_height = img.get('height') + height_value, height_unit = parse_dimension(image_height) + image_width = img.get('width') + width_value, width_unit = parse_dimension(image_width) + image_size = 0 #int(fetch_image_file_size(img,base_url) or 0) + image_format = os.path.splitext(img.get('src',''))[1].lower() + # Remove . from format + image_format = image_format.strip('.') + score = 0 + if height_value: + if height_unit == 'px' and height_value > 150: + score += 1 + if height_unit in ['%','vh','vmin','vmax'] and height_value >30: + score += 1 + if width_value: + if width_unit == 'px' and width_value > 150: + score += 1 + if width_unit in ['%','vh','vmin','vmax'] and width_value >30: + score += 1 + if image_size > 10000: + score += 1 + if img.get('alt') != '': + score+=1 + if any(image_format==format for format in ['jpg','png','webp']): + score+=1 + if index/images_count<0.5: + score+=1 + return score + + if not is_valid_image(img, img.parent, img.parent.get('class', [])): + return None + score = score_image_for_usefulness(img, url, index, total_images) + if score <= IMAGE_SCORE_THRESHOLD: + return None + return { + 'src': img.get('src', ''), + 'alt': img.get('alt', ''), + 'desc': find_closest_parent_with_useful_text(img), + 'score': score, + 'type': 'image' + } + + def process_element(element: element.PageElement) -> bool: + try: + if isinstance(element, NavigableString): + if isinstance(element, Comment): + element.extract() + return False + + if element.name in ['script', 'style', 'link', 'meta', 'noscript']: + if element.name == 'img': + process_image(element, url, 0, 1) + element.decompose() + return False + + keep_element = False + + if element.name == 'a' and element.get('href'): + href = element['href'] + url_base = url.split('/')[2] + link_data = {'href': href, 'text': element.get_text()} + if href.startswith('http') and url_base not in href: + links['external'].append(link_data) + else: + links['internal'].append(link_data) + keep_element = True + + elif element.name == 'img': + return True # Always keep image elements + + elif element.name in ['video', 'audio']: + media[f"{element.name}s"].append({ + 'src': element.get('src'), + 'alt': element.get('alt'), + 'type': element.name, + 'description': find_closest_parent_with_useful_text(element) + }) + source_tags = element.find_all('source') + for source_tag in source_tags: + media[f"{element.name}s"].append({ + 'src': source_tag.get('src'), + 'alt': element.get('alt'), + 'type': element.name, + 'description': find_closest_parent_with_useful_text(element) + }) + return True # Always keep video and audio elements + + if element.name != 'pre': + if element.name in ['b', 'i', 'u', 'span', 'del', 'ins', 'sub', 'sup', 'strong', 'em', 'code', 'kbd', 'var', 's', 'q', 'abbr', 'cite', 'dfn', 'time', 'small', 'mark']: + if kwargs.get('only_text', False): + element.replace_with(element.get_text()) + else: + element.unwrap() + elif element.name != 'img': + element.attrs = {} + + # Process children + for child in list(element.children): + if isinstance(child, NavigableString) and not isinstance(child, Comment): + if len(child.strip()) > 0: + keep_element = True + else: + if process_element(child): + keep_element = True + + + # Check word count + if not keep_element: + word_count = len(element.get_text(strip=True).split()) + keep_element = word_count >= word_count_threshold + + if not keep_element: + element.decompose() + + return keep_element + except Exception as e: + print('Error processing element:', str(e)) + return False + + #process images by filtering and extracting contextual text from the page + # imgs = body.find_all('img') + # media['images'] = [ + # result for result in + # (process_image(img, url, i, len(imgs)) for i, img in enumerate(imgs)) + # if result is not None + # ] + + process_element(body) + + # # Process images using ThreadPoolExecutor + imgs = body.find_all('img') + with ThreadPoolExecutor() as executor: + image_results = list(executor.map(process_image, imgs, [url]*len(imgs), range(len(imgs)), [len(imgs)]*len(imgs))) + media['images'] = [result for result in image_results if result is not None] + + def flatten_nested_elements(node): + if isinstance(node, NavigableString): + return node + if len(node.contents) == 1 and isinstance(node.contents[0], element.Tag) and node.contents[0].name == node.name: + return flatten_nested_elements(node.contents[0]) + node.contents = [flatten_nested_elements(child) for child in node.contents] + return node + + body = flatten_nested_elements(body) + base64_pattern = re.compile(r'data:image/[^;]+;base64,([^"]+)') + for img in imgs: + src = img.get('src', '') + if base64_pattern.match(src): + # Replace base64 data with empty string + img['src'] = base64_pattern.sub('', src) + cleaned_html = str(body).replace('\n\n', '\n').replace(' ', ' ') + cleaned_html = sanitize_html(cleaned_html) + + h = CustomHTML2Text() + h.ignore_links = True + markdown = h.handle(cleaned_html) + markdown = markdown.replace(' ```', '```') + + try: + meta = extract_metadata(html, soup) + except Exception as e: + print('Error extracting metadata:', str(e)) + meta = {} + + return { + 'markdown': markdown, + 'cleaned_html': cleaned_html, + 'success': True, + 'media': media, + 'links': links, + 'metadata': meta + } diff --git a/crawl4ai/extraction_strategy.py b/crawl4ai/extraction_strategy.py index 4f5bb261..53a63310 100644 --- a/crawl4ai/extraction_strategy.py +++ b/crawl4ai/extraction_strategy.py @@ -623,3 +623,158 @@ class ContentSummarizationStrategy(ExtractionStrategy): # Sort summaries by the original section index to maintain order summaries.sort(key=lambda x: x[0]) return [summary for _, summary in summaries] + + +class JsonCssExtractionStrategy(ExtractionStrategy): + def __init__(self, schema: Dict[str, Any], **kwargs): + super().__init__(**kwargs) + self.schema = schema + + def extract(self, url: str, html: str, *q, **kwargs) -> List[Dict[str, Any]]: + soup = BeautifulSoup(html, 'html.parser') + base_elements = soup.select(self.schema['baseSelector']) + + results = [] + for element in base_elements: + item = {} + for field in self.schema['fields']: + value = self._extract_field(element, field) + if value is not None: + item[field['name']] = value + if item: + results.append(item) + + return results + + def _extract_field(self, element, field): + try: + selected = element.select_one(field['selector']) + if not selected: + return None + + if field['type'] == 'text': + return selected.get_text(strip=True) + elif field['type'] == 'attribute': + return selected.get(field['attribute']) + elif field['type'] == 'html': + return str(selected) + elif field['type'] == 'regex': + text = selected.get_text(strip=True) + match = re.search(field['pattern'], text) + return match.group(1) if match else None + except Exception as e: + if self.verbose: + print(f"Error extracting field {field['name']}: {str(e)}") + return None + + def run(self, url: str, sections: List[str], *q, **kwargs) -> List[Dict[str, Any]]: + combined_html = self.DEL.join(sections) + return self.extract(url, combined_html, **kwargs) + + +class EnhancedJsonCssExtractionStrategy(ExtractionStrategy): + def __init__(self, schema: Dict[str, Any], **kwargs): + super().__init__(**kwargs) + self.schema = schema + + def extract(self, url: str, html: str, *q, **kwargs) -> List[Dict[str, Any]]: + soup = BeautifulSoup(html, 'html.parser') + base_elements = soup.select(self.schema['baseSelector']) + + results = [] + for element in base_elements: + item = self._extract_item(element, self.schema['fields']) + if item: + results.append(item) + + return results + + + + def _extract_field(self, element, field): + try: + if field['type'] == 'nested': + nested_element = element.select_one(field['selector']) + return self._extract_item(nested_element, field['fields']) if nested_element else {} + + if field['type'] == 'list': + elements = element.select(field['selector']) + return [self._extract_list_item(el, field['fields']) for el in elements] + + if field['type'] == 'nested_list': + elements = element.select(field['selector']) + return [self._extract_item(el, field['fields']) for el in elements] + + return self._extract_single_field(element, field) + except Exception as e: + if self.verbose: + print(f"Error extracting field {field['name']}: {str(e)}") + return field.get('default') + + def _extract_list_item(self, element, fields): + item = {} + for field in fields: + value = self._extract_single_field(element, field) + if value is not None: + item[field['name']] = value + return item + + def _extract_single_field(self, element, field): + if 'selector' in field: + selected = element.select_one(field['selector']) + if not selected: + return field.get('default') + else: + selected = element + + value = None + if field['type'] == 'text': + value = selected.get_text(strip=True) + elif field['type'] == 'attribute': + value = selected.get(field['attribute']) + elif field['type'] == 'html': + value = str(selected) + elif field['type'] == 'regex': + text = selected.get_text(strip=True) + match = re.search(field['pattern'], text) + value = match.group(1) if match else None + + if 'transform' in field: + value = self._apply_transform(value, field['transform']) + + return value if value is not None else field.get('default') + + def _extract_item(self, element, fields): + item = {} + for field in fields: + if field['type'] == 'computed': + value = self._compute_field(item, field) + else: + value = self._extract_field(element, field) + if value is not None: + item[field['name']] = value + return item + + def _apply_transform(self, value, transform): + if transform == 'lowercase': + return value.lower() + elif transform == 'uppercase': + return value.upper() + elif transform == 'strip': + return value.strip() + return value + + def _compute_field(self, item, field): + try: + if 'expression' in field: + return eval(field['expression'], {}, item) + elif 'function' in field: + return field['function'](item) + except Exception as e: + if self.verbose: + print(f"Error computing field {field['name']}: {str(e)}") + return field.get('default') + + def run(self, url: str, sections: List[str], *q, **kwargs) -> List[Dict[str, Any]]: + combined_html = self.DEL.join(sections) + return self.extract(url, combined_html, **kwargs) \ No newline at end of file diff --git a/crawl4ai/models.py b/crawl4ai/models.py index f844b23c..e48441b8 100644 --- a/crawl4ai/models.py +++ b/crawl4ai/models.py @@ -16,4 +16,5 @@ class CrawlResult(BaseModel): markdown: Optional[str] = None extracted_content: Optional[str] = None metadata: Optional[dict] = None - error_message: Optional[str] = None \ No newline at end of file + error_message: Optional[str] = None + session_id: Optional[str] = None \ No newline at end of file diff --git a/crawl4ai/web_crawler.py b/crawl4ai/web_crawler.py index 351fa62e..3eda6b45 100644 --- a/crawl4ai/web_crawler.py +++ b/crawl4ai/web_crawler.py @@ -122,7 +122,7 @@ class WebCrawler: if not isinstance(chunking_strategy, ChunkingStrategy): raise ValueError("Unsupported chunking strategy") - word_count_threshold = max(word_count_threshold, 0) + word_count_threshold = max(word_count_threshold, MIN_WORD_THRESHOLD) cached = None screenshot_data = None diff --git a/tests/async/test_basic_crawling.py b/tests/async/test_basic_crawling.py new file mode 100644 index 00000000..7184f464 --- /dev/null +++ b/tests/async/test_basic_crawling.py @@ -0,0 +1,81 @@ +import os +import sys +import pytest +import asyncio +import time + +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler + +@pytest.mark.asyncio +async def test_successful_crawl(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert result.url == url + assert result.html + assert result.markdown + assert result.cleaned_html + +@pytest.mark.asyncio +async def test_invalid_url(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.invalidurl12345.com" + result = await crawler.arun(url=url, bypass_cache=True) + assert not result.success + assert result.error_message + +@pytest.mark.asyncio +async def test_multiple_urls(): + async with AsyncWebCrawler(verbose=True) as crawler: + urls = [ + "https://www.nbcnews.com/business", + "https://www.example.com", + "https://www.python.org" + ] + results = await crawler.arun_many(urls=urls, bypass_cache=True) + assert len(results) == len(urls) + assert all(result.success for result in results) + assert all(result.html for result in results) + +@pytest.mark.asyncio +async def test_javascript_execution(): + async with AsyncWebCrawler(verbose=True) as crawler: + js_code = "document.body.innerHTML = '

Modified by JS

';" + url = "https://www.example.com" + result = await crawler.arun(url=url, bypass_cache=True, js_code=js_code) + assert result.success + assert "

Modified by JS

" in result.html + +@pytest.mark.asyncio +async def test_concurrent_crawling_performance(): + async with AsyncWebCrawler(verbose=True) as crawler: + urls = [ + "https://www.nbcnews.com/business", + "https://www.example.com", + "https://www.python.org", + "https://www.github.com", + "https://www.stackoverflow.com" + ] + + start_time = time.time() + results = await crawler.arun_many(urls=urls, bypass_cache=True) + end_time = time.time() + + total_time = end_time - start_time + print(f"Total time for concurrent crawling: {total_time:.2f} seconds") + + assert all(result.success for result in results) + assert len(results) == len(urls) + + # Assert that concurrent crawling is faster than sequential + # This multiplier may need adjustment based on the number of URLs and their complexity + assert total_time < len(urls) * 5, f"Concurrent crawling not significantly faster: {total_time:.2f} seconds" + +# Entry point for debugging +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_caching.py b/tests/async/test_caching.py new file mode 100644 index 00000000..589beca9 --- /dev/null +++ b/tests/async/test_caching.py @@ -0,0 +1,82 @@ +import os +import sys +import pytest +import asyncio + +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler + +@pytest.mark.asyncio +async def test_caching(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + + # First crawl (should not use cache) + start_time = asyncio.get_event_loop().time() + result1 = await crawler.arun(url=url, bypass_cache=True) + end_time = asyncio.get_event_loop().time() + time_taken1 = end_time - start_time + + assert result1.success + + # Second crawl (should use cache) + start_time = asyncio.get_event_loop().time() + result2 = await crawler.arun(url=url, bypass_cache=False) + end_time = asyncio.get_event_loop().time() + time_taken2 = end_time - start_time + + assert result2.success + assert time_taken2 < time_taken1 # Cached result should be faster + +@pytest.mark.asyncio +async def test_bypass_cache(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + + # First crawl + result1 = await crawler.arun(url=url, bypass_cache=False) + assert result1.success + + # Second crawl with bypass_cache=True + result2 = await crawler.arun(url=url, bypass_cache=True) + assert result2.success + + # Content should be different (or at least, not guaranteed to be the same) + assert result1.html != result2.html or result1.markdown != result2.markdown + +@pytest.mark.asyncio +async def test_clear_cache(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + + # Crawl and cache + await crawler.arun(url=url, bypass_cache=False) + + # Clear cache + await crawler.aclear_cache() + + # Check cache size + cache_size = await crawler.aget_cache_size() + assert cache_size == 0 + +@pytest.mark.asyncio +async def test_flush_cache(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + + # Crawl and cache + await crawler.arun(url=url, bypass_cache=False) + + # Flush cache + await crawler.aflush_cache() + + # Check cache size + cache_size = await crawler.aget_cache_size() + assert cache_size == 0 + +# Entry point for debugging +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_chunking_and_extraction_strategies.py b/tests/async/test_chunking_and_extraction_strategies.py new file mode 100644 index 00000000..a23fb4aa --- /dev/null +++ b/tests/async/test_chunking_and_extraction_strategies.py @@ -0,0 +1,87 @@ +import os +import sys +import pytest +import asyncio +import json + +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler +from crawl4ai.chunking_strategy import RegexChunking, NlpSentenceChunking +from crawl4ai.extraction_strategy import CosineStrategy, LLMExtractionStrategy + +@pytest.mark.asyncio +async def test_regex_chunking(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + chunking_strategy = RegexChunking(patterns=["\n\n"]) + result = await crawler.arun( + url=url, + chunking_strategy=chunking_strategy, + bypass_cache=True + ) + assert result.success + assert result.extracted_content + chunks = json.loads(result.extracted_content) + assert len(chunks) > 1 # Ensure multiple chunks were created + +@pytest.mark.asyncio +async def test_cosine_strategy(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + extraction_strategy = CosineStrategy(word_count_threshold=10, max_dist=0.2, linkage_method="ward", top_k=3, sim_threshold=0.3) + result = await crawler.arun( + url=url, + extraction_strategy=extraction_strategy, + bypass_cache=True + ) + assert result.success + assert result.extracted_content + extracted_data = json.loads(result.extracted_content) + assert len(extracted_data) > 0 + assert all('tags' in item for item in extracted_data) + +@pytest.mark.asyncio +async def test_llm_extraction_strategy(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + extraction_strategy = LLMExtractionStrategy( + provider="openai/gpt-4o-mini", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Extract only content related to technology" + ) + result = await crawler.arun( + url=url, + extraction_strategy=extraction_strategy, + bypass_cache=True + ) + assert result.success + assert result.extracted_content + extracted_data = json.loads(result.extracted_content) + assert len(extracted_data) > 0 + assert all('content' in item for item in extracted_data) + +@pytest.mark.asyncio +async def test_combined_chunking_and_extraction(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + chunking_strategy = RegexChunking(patterns=["\n\n"]) + extraction_strategy = CosineStrategy(word_count_threshold=10, max_dist=0.2, linkage_method="ward", top_k=3, sim_threshold=0.3) + result = await crawler.arun( + url=url, + chunking_strategy=chunking_strategy, + extraction_strategy=extraction_strategy, + bypass_cache=True + ) + assert result.success + assert result.extracted_content + extracted_data = json.loads(result.extracted_content) + assert len(extracted_data) > 0 + assert all('tags' in item for item in extracted_data) + assert all('content' in item for item in extracted_data) + +# Entry point for debugging +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_content_extraction.py b/tests/async/test_content_extraction.py new file mode 100644 index 00000000..7604db20 --- /dev/null +++ b/tests/async/test_content_extraction.py @@ -0,0 +1,90 @@ +import os +import sys +import pytest +import asyncio +import json + +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler + +@pytest.mark.asyncio +async def test_extract_markdown(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert result.markdown + assert isinstance(result.markdown, str) + assert len(result.markdown) > 0 + +@pytest.mark.asyncio +async def test_extract_cleaned_html(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert result.cleaned_html + assert isinstance(result.cleaned_html, str) + assert len(result.cleaned_html) > 0 + +@pytest.mark.asyncio +async def test_extract_media(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert result.media + media = result.media + assert isinstance(media, dict) + assert "images" in media + assert isinstance(media["images"], list) + for image in media["images"]: + assert "src" in image + assert "alt" in image + assert "type" in image + +@pytest.mark.asyncio +async def test_extract_links(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert result.links + links = result.links + assert isinstance(links, dict) + assert "internal" in links + assert "external" in links + assert isinstance(links["internal"], list) + assert isinstance(links["external"], list) + for link in links["internal"] + links["external"]: + assert "href" in link + assert "text" in link + +@pytest.mark.asyncio +async def test_extract_metadata(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert result.metadata + metadata = result.metadata + assert isinstance(metadata, dict) + assert "title" in metadata + assert isinstance(metadata["title"], str) + +@pytest.mark.asyncio +async def test_css_selector_extraction(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + css_selector = "h1, h2, h3" + result = await crawler.arun(url=url, bypass_cache=True, css_selector=css_selector) + assert result.success + assert result.markdown + assert all(heading in result.markdown for heading in ["#", "##", "###"]) + +# Entry point for debugging +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_crawler_strategy.py b/tests/async/test_crawler_strategy.py new file mode 100644 index 00000000..a507058d --- /dev/null +++ b/tests/async/test_crawler_strategy.py @@ -0,0 +1,68 @@ +import os +import sys +import pytest +import asyncio + +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler +from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy + +@pytest.mark.asyncio +async def test_custom_user_agent(): + async with AsyncWebCrawler(verbose=True) as crawler: + custom_user_agent = "MyCustomUserAgent/1.0" + crawler.crawler_strategy.update_user_agent(custom_user_agent) + url = "https://httpbin.org/user-agent" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert custom_user_agent in result.html + +@pytest.mark.asyncio +async def test_custom_headers(): + async with AsyncWebCrawler(verbose=True) as crawler: + custom_headers = {"X-Test-Header": "TestValue"} + crawler.crawler_strategy.set_custom_headers(custom_headers) + url = "https://httpbin.org/headers" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert "X-Test-Header" in result.html + assert "TestValue" in result.html + +@pytest.mark.asyncio +async def test_javascript_execution(): + async with AsyncWebCrawler(verbose=True) as crawler: + js_code = "document.body.innerHTML = '

Modified by JS

';" + url = "https://www.example.com" + result = await crawler.arun(url=url, bypass_cache=True, js_code=js_code) + assert result.success + assert "

Modified by JS

" in result.html + +@pytest.mark.asyncio +async def test_hook_execution(): + async with AsyncWebCrawler(verbose=True) as crawler: + async def test_hook(page): + await page.evaluate("document.body.style.backgroundColor = 'red';") + return page + + crawler.crawler_strategy.set_hook('after_goto', test_hook) + url = "https://www.example.com" + result = await crawler.arun(url=url, bypass_cache=True) + assert result.success + assert "background-color: red" in result.html + +@pytest.mark.asyncio +async def test_screenshot(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.example.com" + result = await crawler.arun(url=url, bypass_cache=True, screenshot=True) + assert result.success + assert result.screenshot + assert isinstance(result.screenshot, str) + assert len(result.screenshot) > 0 + +# Entry point for debugging +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_database_operations.py b/tests/async/test_database_operations.py new file mode 100644 index 00000000..90a09ff0 --- /dev/null +++ b/tests/async/test_database_operations.py @@ -0,0 +1,82 @@ +import os +import sys +import pytest +import asyncio +import json + +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler + +@pytest.mark.asyncio +async def test_cache_url(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.example.com" + # First run to cache the URL + result1 = await crawler.arun(url=url, bypass_cache=True) + assert result1.success + + # Second run to retrieve from cache + result2 = await crawler.arun(url=url, bypass_cache=False) + assert result2.success + assert result2.html == result1.html + +@pytest.mark.asyncio +async def test_bypass_cache(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.python.org" + # First run to cache the URL + result1 = await crawler.arun(url=url, bypass_cache=True) + assert result1.success + + # Second run bypassing cache + result2 = await crawler.arun(url=url, bypass_cache=True) + assert result2.success + assert result2.html != result1.html # Content might be different due to dynamic nature of websites + +@pytest.mark.asyncio +async def test_cache_size(): + async with AsyncWebCrawler(verbose=True) as crawler: + initial_size = await crawler.aget_cache_size() + + url = "https://www.nbcnews.com/business" + await crawler.arun(url=url, bypass_cache=True) + + new_size = await crawler.aget_cache_size() + assert new_size == initial_size + 1 + +@pytest.mark.asyncio +async def test_clear_cache(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.example.org" + await crawler.arun(url=url, bypass_cache=True) + + initial_size = await crawler.aget_cache_size() + assert initial_size > 0 + + await crawler.aclear_cache() + new_size = await crawler.aget_cache_size() + assert new_size == 0 + +@pytest.mark.asyncio +async def test_flush_cache(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.example.net" + await crawler.arun(url=url, bypass_cache=True) + + initial_size = await crawler.aget_cache_size() + assert initial_size > 0 + + await crawler.aflush_cache() + new_size = await crawler.aget_cache_size() + assert new_size == 0 + + # Try to retrieve the previously cached URL + result = await crawler.arun(url=url, bypass_cache=False) + assert result.success # The crawler should still succeed, but it will fetch the content anew + +# Entry point for debugging +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_edge_cases.py b/tests/async/test_edge_cases.py new file mode 100644 index 00000000..34fadb1e --- /dev/null +++ b/tests/async/test_edge_cases.py @@ -0,0 +1,127 @@ +import os +import re +import sys +import pytest +import json +from bs4 import BeautifulSoup +import asyncio +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler + +# @pytest.mark.asyncio +# async def test_large_content_page(): +# async with AsyncWebCrawler(verbose=True) as crawler: +# url = "https://en.wikipedia.org/wiki/List_of_largest_known_stars" # A page with a large table +# result = await crawler.arun(url=url, bypass_cache=True) +# assert result.success +# assert len(result.html) > 1000000 # Expecting more than 1MB of content + +# @pytest.mark.asyncio +# async def test_minimal_content_page(): +# async with AsyncWebCrawler(verbose=True) as crawler: +# url = "https://example.com" # A very simple page +# result = await crawler.arun(url=url, bypass_cache=True) +# assert result.success +# assert len(result.html) < 10000 # Expecting less than 10KB of content + +# @pytest.mark.asyncio +# async def test_single_page_application(): +# async with AsyncWebCrawler(verbose=True) as crawler: +# url = "https://reactjs.org/" # React's website is a SPA +# result = await crawler.arun(url=url, bypass_cache=True) +# assert result.success +# assert "react" in result.html.lower() + +# @pytest.mark.asyncio +# async def test_page_with_infinite_scroll(): +# async with AsyncWebCrawler(verbose=True) as crawler: +# url = "https://news.ycombinator.com/" # Hacker News has infinite scroll +# result = await crawler.arun(url=url, bypass_cache=True) +# assert result.success +# assert "hacker news" in result.html.lower() + +# @pytest.mark.asyncio +# async def test_page_with_heavy_javascript(): +# async with AsyncWebCrawler(verbose=True) as crawler: +# url = "https://www.airbnb.com/" # Airbnb uses a lot of JavaScript +# result = await crawler.arun(url=url, bypass_cache=True) +# assert result.success +# assert "airbnb" in result.html.lower() + +# @pytest.mark.asyncio +# async def test_page_with_mixed_content(): +# async with AsyncWebCrawler(verbose=True) as crawler: +# url = "https://github.com/" # GitHub has a mix of static and dynamic content +# result = await crawler.arun(url=url, bypass_cache=True) +# assert result.success +# assert "github" in result.html.lower() + +# Add this test to your existing test file +@pytest.mark.asyncio +async def test_typescript_commits_multi_page(): + first_commit = "" + async def on_execution_started(page): + nonlocal first_commit + try: + # Check if the page firct commit h4 text is different from the first commit (use document.querySelector('li.Box-sc-g0xbh4-0 h4')) + while True: + await page.wait_for_selector('li.Box-sc-g0xbh4-0 h4') + commit = await page.query_selector('li.Box-sc-g0xbh4-0 h4') + commit = await commit.evaluate('(element) => element.textContent') + commit = re.sub(r'\s+', '', commit) + if commit and commit != first_commit: + first_commit = commit + break + await asyncio.sleep(0.5) + except Exception as e: + print(f"Warning: New content didn't appear after JavaScript execution: {e}") + + + async with AsyncWebCrawler(verbose=True) as crawler: + crawler.crawler_strategy.set_hook('on_execution_started', on_execution_started) + + url = "https://github.com/microsoft/TypeScript/commits/main" + session_id = "typescript_commits_session" + all_commits = [] + + js_next_page = """ + const button = document.querySelector('a[data-testid="pagination-next-button"]'); + if (button) button.click(); + """ + + for page in range(3): # Crawl 3 pages + result = await crawler.arun( + url=url, # Only use URL for the first page + session_id=session_id, + css_selector="li.Box-sc-g0xbh4-0", + js=js_next_page if page > 0 else None, # Don't click 'next' on the first page + bypass_cache=True, + js_only=page > 0 # Use js_only for subsequent pages + ) + + assert result.success, f"Failed to crawl page {page + 1}" + + # Parse the HTML and extract commits + soup = BeautifulSoup(result.cleaned_html, 'html.parser') + commits = soup.select("li") + # Take first commit find h4 extract text + first_commit = commits[0].find("h4").text + first_commit = re.sub(r'\s+', '', first_commit) + all_commits.extend(commits) + + print(f"Page {page + 1}: Found {len(commits)} commits") + + # Clean up the session + await crawler.crawler_strategy.kill_session(session_id) + + # Assertions + assert len(all_commits) >= 90, f"Expected at least 90 commits, but got {len(all_commits)}" + + print(f"Successfully crawled {len(all_commits)} commits across 3 pages") + +# Entry point for debugging +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_error_handling.py b/tests/async/test_error_handling.py new file mode 100644 index 00000000..3015edbd --- /dev/null +++ b/tests/async/test_error_handling.py @@ -0,0 +1,78 @@ +# import os +# import sys +# import pytest +# import asyncio + +# # Add the parent directory to the Python path +# parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# sys.path.append(parent_dir) + +# from crawl4ai.async_webcrawler import AsyncWebCrawler +# from crawl4ai.utils import InvalidCSSSelectorError + +# class AsyncCrawlerWrapper: +# def __init__(self): +# self.crawler = None + +# async def setup(self): +# self.crawler = AsyncWebCrawler(verbose=True) +# await self.crawler.awarmup() + +# async def cleanup(self): +# if self.crawler: +# await self.crawler.aclear_cache() + +# @pytest.fixture(scope="module") +# def crawler_wrapper(): +# wrapper = AsyncCrawlerWrapper() +# asyncio.get_event_loop().run_until_complete(wrapper.setup()) +# yield wrapper +# asyncio.get_event_loop().run_until_complete(wrapper.cleanup()) + +# @pytest.mark.asyncio +# async def test_network_error(crawler_wrapper): +# url = "https://www.nonexistentwebsite123456789.com" +# result = await crawler_wrapper.crawler.arun(url=url, bypass_cache=True) +# assert not result.success +# assert "Failed to crawl" in result.error_message + +# # @pytest.mark.asyncio +# # async def test_timeout_error(crawler_wrapper): +# # # Simulating a timeout by using a very short timeout value +# # url = "https://www.nbcnews.com/business" +# # result = await crawler_wrapper.crawler.arun(url=url, bypass_cache=True, timeout=0.001) +# # assert not result.success +# # assert "timeout" in result.error_message.lower() + +# # @pytest.mark.asyncio +# # async def test_invalid_css_selector(crawler_wrapper): +# # url = "https://www.nbcnews.com/business" +# # with pytest.raises(InvalidCSSSelectorError): +# # await crawler_wrapper.crawler.arun(url=url, bypass_cache=True, css_selector="invalid>>selector") + +# # @pytest.mark.asyncio +# # async def test_js_execution_error(crawler_wrapper): +# # url = "https://www.nbcnews.com/business" +# # invalid_js = "This is not valid JavaScript code;" +# # result = await crawler_wrapper.crawler.arun(url=url, bypass_cache=True, js=invalid_js) +# # assert not result.success +# # assert "JavaScript" in result.error_message + +# # @pytest.mark.asyncio +# # async def test_empty_page(crawler_wrapper): +# # # Use a URL that typically returns an empty page +# # url = "http://example.com/empty" +# # result = await crawler_wrapper.crawler.arun(url=url, bypass_cache=True) +# # assert result.success # The crawl itself should succeed +# # assert not result.markdown.strip() # The markdown content should be empty or just whitespace + +# # @pytest.mark.asyncio +# # async def test_rate_limiting(crawler_wrapper): +# # # Simulate rate limiting by making multiple rapid requests +# # url = "https://www.nbcnews.com/business" +# # results = await asyncio.gather(*[crawler_wrapper.crawler.arun(url=url, bypass_cache=True) for _ in range(10)]) +# # assert any(not result.success and "rate limit" in result.error_message.lower() for result in results) + +# # Entry point for debugging +# if __name__ == "__main__": +# pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_parameters_and_options.py b/tests/async/test_parameters_and_options.py new file mode 100644 index 00000000..8ae7c1d3 --- /dev/null +++ b/tests/async/test_parameters_and_options.py @@ -0,0 +1,94 @@ +import os +import sys +import pytest +import asyncio +import json + +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler + +@pytest.mark.asyncio +async def test_word_count_threshold(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result_no_threshold = await crawler.arun(url=url, word_count_threshold=0, bypass_cache=True) + result_with_threshold = await crawler.arun(url=url, word_count_threshold=50, bypass_cache=True) + + assert len(result_no_threshold.markdown) > len(result_with_threshold.markdown) + +@pytest.mark.asyncio +async def test_css_selector(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + css_selector = "h1, h2, h3" + result = await crawler.arun(url=url, css_selector=css_selector, bypass_cache=True) + + assert result.success + assert " button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"] + result_with_more = await crawler.arun(url=url, js=js_code, bypass_cache=True) + + assert result_with_more.success + assert len(result_with_more.markdown) > len(result_without_more.markdown) + +@pytest.mark.asyncio +async def test_screenshot(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, screenshot=True, bypass_cache=True) + + assert result.success + assert result.screenshot + assert isinstance(result.screenshot, str) # Should be a base64 encoded string + +@pytest.mark.asyncio +async def test_custom_user_agent(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + custom_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Crawl4AI/1.0" + result = await crawler.arun(url=url, user_agent=custom_user_agent, bypass_cache=True) + + assert result.success + # Note: We can't directly verify the user agent in the result, but we can check if the crawl was successful + +@pytest.mark.asyncio +async def test_extract_media_and_links(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, bypass_cache=True) + + assert result.success + assert result.media + assert isinstance(result.media, dict) + assert 'images' in result.media + assert result.links + assert isinstance(result.links, dict) + assert 'internal' in result.links and 'external' in result.links + +@pytest.mark.asyncio +async def test_metadata_extraction(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + result = await crawler.arun(url=url, bypass_cache=True) + + assert result.success + assert result.metadata + assert isinstance(result.metadata, dict) + # Check for common metadata fields + assert any(key in result.metadata for key in ['title', 'description', 'keywords']) + +# Entry point for debugging +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/async/test_performance.py b/tests/async/test_performance.py new file mode 100644 index 00000000..9528b5ab --- /dev/null +++ b/tests/async/test_performance.py @@ -0,0 +1,72 @@ +import os +import sys +import pytest +import asyncio +import time + +# Add the parent directory to the Python path +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +from crawl4ai.async_webcrawler import AsyncWebCrawler + +@pytest.mark.asyncio +async def test_crawl_speed(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + start_time = time.time() + result = await crawler.arun(url=url, bypass_cache=True) + end_time = time.time() + + assert result.success + crawl_time = end_time - start_time + print(f"Crawl time: {crawl_time:.2f} seconds") + + assert crawl_time < 10, f"Crawl took too long: {crawl_time:.2f} seconds" + +@pytest.mark.asyncio +async def test_concurrent_crawling_performance(): + async with AsyncWebCrawler(verbose=True) as crawler: + urls = [ + "https://www.nbcnews.com/business", + "https://www.example.com", + "https://www.python.org", + "https://www.github.com", + "https://www.stackoverflow.com" + ] + + start_time = time.time() + results = await crawler.arun_many(urls=urls, bypass_cache=True) + end_time = time.time() + + total_time = end_time - start_time + print(f"Total time for concurrent crawling: {total_time:.2f} seconds") + + assert all(result.success for result in results) + assert len(results) == len(urls) + + assert total_time < len(urls) * 5, f"Concurrent crawling not significantly faster: {total_time:.2f} seconds" + +@pytest.mark.asyncio +async def test_crawl_speed_with_caching(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.nbcnews.com/business" + + start_time = time.time() + result1 = await crawler.arun(url=url, bypass_cache=True) + end_time = time.time() + first_crawl_time = end_time - start_time + + start_time = time.time() + result2 = await crawler.arun(url=url, bypass_cache=False) + end_time = time.time() + second_crawl_time = end_time - start_time + + assert result1.success and result2.success + print(f"First crawl time: {first_crawl_time:.2f} seconds") + print(f"Second crawl time (cached): {second_crawl_time:.2f} seconds") + + assert second_crawl_time < first_crawl_time / 2, "Cached crawl not significantly faster" + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file From 2fada16abbd8ac4aedf5fa650cfddc935651d207 Mon Sep 17 00:00:00 2001 From: unclecode Date: Tue, 3 Sep 2024 23:32:27 +0800 Subject: [PATCH 10/57] chore: Update crawl4ai package with AsyncWebCrawler and JsonCssExtractionStrategy --- crawl4ai/__init__.py | 12 +- crawl4ai/async_webcrawler.py | 8 +- crawl4ai/extraction_strategy.py | 183 +++++++++++++++++++------- docs/examples/crawlai_vs_firecrawl.py | 67 ++++++++++ 4 files changed, 216 insertions(+), 54 deletions(-) create mode 100644 docs/examples/crawlai_vs_firecrawl.py diff --git a/crawl4ai/__init__.py b/crawl4ai/__init__.py index 0ef0049d..9c5ac3e0 100644 --- a/crawl4ai/__init__.py +++ b/crawl4ai/__init__.py @@ -1 +1,11 @@ -from .web_crawler import WebCrawler \ No newline at end of file +from .web_crawler import WebCrawler +from .async_webcrawler import AsyncWebCrawler +from .models import CrawlResult + +__version__ = "0.2.77" + +__all__ = [ + "WebCrawler", + "AsyncWebCrawler", + "CrawlResult", +] diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index 212d59ca..3cdc9ac1 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -44,7 +44,8 @@ class AsyncWebCrawler: await self.crawler_strategy.__aexit__(exc_type, exc_val, exc_tb) async def awarmup(self): - print("[LOG] 🌤️ Warming up the AsyncWebCrawler") + if self.verbose: + print("[LOG] 🌤️ Warming up the AsyncWebCrawler") await async_db_manager.ainit_db() await self.arun( url="https://google.com/", @@ -53,7 +54,8 @@ class AsyncWebCrawler: verbose=False, ) self.ready = True - print("[LOG] 🌞 AsyncWebCrawler is ready to crawl") + if self.verbose: + print("[LOG] 🌞 AsyncWebCrawler is ready to crawl") async def arun( self, @@ -215,7 +217,7 @@ class AsyncWebCrawler: ) # Check if extraction strategy is type of JsonCssExtractionStrategy - if isinstance(extraction_strategy, JsonCssExtractionStrategy) or isinstance(extraction_strategy, EnhancedJsonCssExtractionStrategy): + if isinstance(extraction_strategy, JsonCssExtractionStrategy) or isinstance(extraction_strategy, JsonCssExtractionStrategy): extraction_strategy.verbose = verbose extracted_content = extraction_strategy.run(url, [html]) extracted_content = json.dumps(extracted_content, indent=4, default=str) diff --git a/crawl4ai/extraction_strategy.py b/crawl4ai/extraction_strategy.py index 53a63310..48491067 100644 --- a/crawl4ai/extraction_strategy.py +++ b/crawl4ai/extraction_strategy.py @@ -10,7 +10,7 @@ from functools import partial from .model_loader import * import math import numpy as np - +from lxml import etree class ExtractionStrategy(ABC): """ @@ -623,60 +623,12 @@ class ContentSummarizationStrategy(ExtractionStrategy): # Sort summaries by the original section index to maintain order summaries.sort(key=lambda x: x[0]) return [summary for _, summary in summaries] - - + class JsonCssExtractionStrategy(ExtractionStrategy): def __init__(self, schema: Dict[str, Any], **kwargs): super().__init__(**kwargs) self.schema = schema - def extract(self, url: str, html: str, *q, **kwargs) -> List[Dict[str, Any]]: - soup = BeautifulSoup(html, 'html.parser') - base_elements = soup.select(self.schema['baseSelector']) - - results = [] - for element in base_elements: - item = {} - for field in self.schema['fields']: - value = self._extract_field(element, field) - if value is not None: - item[field['name']] = value - if item: - results.append(item) - - return results - - def _extract_field(self, element, field): - try: - selected = element.select_one(field['selector']) - if not selected: - return None - - if field['type'] == 'text': - return selected.get_text(strip=True) - elif field['type'] == 'attribute': - return selected.get(field['attribute']) - elif field['type'] == 'html': - return str(selected) - elif field['type'] == 'regex': - text = selected.get_text(strip=True) - match = re.search(field['pattern'], text) - return match.group(1) if match else None - except Exception as e: - if self.verbose: - print(f"Error extracting field {field['name']}: {str(e)}") - return None - - def run(self, url: str, sections: List[str], *q, **kwargs) -> List[Dict[str, Any]]: - combined_html = self.DEL.join(sections) - return self.extract(url, combined_html, **kwargs) - - -class EnhancedJsonCssExtractionStrategy(ExtractionStrategy): - def __init__(self, schema: Dict[str, Any], **kwargs): - super().__init__(**kwargs) - self.schema = schema - def extract(self, url: str, html: str, *q, **kwargs) -> List[Dict[str, Any]]: soup = BeautifulSoup(html, 'html.parser') base_elements = soup.select(self.schema['baseSelector']) @@ -775,6 +727,137 @@ class EnhancedJsonCssExtractionStrategy(ExtractionStrategy): print(f"Error computing field {field['name']}: {str(e)}") return field.get('default') + def run(self, url: str, sections: List[str], *q, **kwargs) -> List[Dict[str, Any]]: + combined_html = self.DEL.join(sections) + return self.extract(url, combined_html, **kwargs) + + +class JsonXPATHExtractionStrategy(ExtractionStrategy): + def __init__(self, schema: Dict[str, Any], **kwargs): + super().__init__(**kwargs) + self.schema = schema + self.use_cssselect = self._check_cssselect() + + def _check_cssselect(self): + try: + import cssselect + return True + except ImportError: + print("Warning: cssselect is not installed. Falling back to XPath for all selectors.") + return False + + def extract(self, url: str, html: str, *q, **kwargs) -> List[Dict[str, Any]]: + self.soup = BeautifulSoup(html, 'lxml') + self.tree = etree.HTML(str(self.soup)) + + selector_type = 'xpath' if not self.use_cssselect else self.schema.get('selectorType', 'css') + base_selector = self.schema.get('baseXPath' if selector_type == 'xpath' else 'baseSelector') + base_elements = self._select_elements(base_selector, selector_type) + + results = [] + for element in base_elements: + item = self._extract_item(element, self.schema['fields']) + if item: + results.append(item) + + return results + + def _select_elements(self, selector, selector_type, element=None): + if selector_type == 'xpath' or not self.use_cssselect: + return self.tree.xpath(selector) if element is None else element.xpath(selector) + else: # CSS + return self.tree.cssselect(selector) if element is None else element.cssselect(selector) + + def _extract_field(self, element, field): + try: + selector_type = 'xpath' if not self.use_cssselect else field.get('selectorType', 'css') + selector = field.get('xpathSelector' if selector_type == 'xpath' else 'selector') + + if field['type'] == 'nested': + nested_element = self._select_elements(selector, selector_type, element) + return self._extract_item(nested_element[0], field['fields']) if nested_element else {} + + if field['type'] == 'list': + elements = self._select_elements(selector, selector_type, element) + return [self._extract_list_item(el, field['fields']) for el in elements] + + if field['type'] == 'nested_list': + elements = self._select_elements(selector, selector_type, element) + return [self._extract_item(el, field['fields']) for el in elements] + + return self._extract_single_field(element, field) + except Exception as e: + if self.verbose: + print(f"Error extracting field {field['name']}: {str(e)}") + return field.get('default') + + def _extract_list_item(self, element, fields): + item = {} + for field in fields: + value = self._extract_single_field(element, field) + if value is not None: + item[field['name']] = value + return item + + def _extract_single_field(self, element, field): + selector_type = field.get('selectorType', 'css') + + if 'selector' in field: + selected = self._select_elements(field['selector'], selector_type, element) + if not selected: + return field.get('default') + selected = selected[0] + else: + selected = element + + value = None + if field['type'] == 'text': + value = selected.text_content().strip() if hasattr(selected, 'text_content') else selected.text.strip() + elif field['type'] == 'attribute': + value = selected.get(field['attribute']) + elif field['type'] == 'html': + value = etree.tostring(selected, encoding='unicode') + elif field['type'] == 'regex': + text = selected.text_content().strip() if hasattr(selected, 'text_content') else selected.text.strip() + match = re.search(field['pattern'], text) + value = match.group(1) if match else None + + if 'transform' in field: + value = self._apply_transform(value, field['transform']) + + return value if value is not None else field.get('default') + + def _extract_item(self, element, fields): + item = {} + for field in fields: + if field['type'] == 'computed': + value = self._compute_field(item, field) + else: + value = self._extract_field(element, field) + if value is not None: + item[field['name']] = value + return item + + def _apply_transform(self, value, transform): + if transform == 'lowercase': + return value.lower() + elif transform == 'uppercase': + return value.upper() + elif transform == 'strip': + return value.strip() + return value + + def _compute_field(self, item, field): + try: + if 'expression' in field: + return eval(field['expression'], {}, item) + elif 'function' in field: + return field['function'](item) + except Exception as e: + if self.verbose: + print(f"Error computing field {field['name']}: {str(e)}") + return field.get('default') + def run(self, url: str, sections: List[str], *q, **kwargs) -> List[Dict[str, Any]]: combined_html = self.DEL.join(sections) return self.extract(url, combined_html, **kwargs) \ No newline at end of file diff --git a/docs/examples/crawlai_vs_firecrawl.py b/docs/examples/crawlai_vs_firecrawl.py new file mode 100644 index 00000000..b50b06da --- /dev/null +++ b/docs/examples/crawlai_vs_firecrawl.py @@ -0,0 +1,67 @@ +import os, time +# append the path to the root of the project +import sys +import asyncio +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from firecrawl import FirecrawlApp +from crawl4ai import AsyncWebCrawler +__data__ = os.path.join(os.path.dirname(__file__), '..', '..') + '/.data' + +async def compare(): + app = FirecrawlApp(api_key=os.environ['FIRECRAWL_API_KEY']) + + # Tet Firecrawl with a simple crawl + start = time.time() + scrape_status = app.scrape_url( + 'https://www.nbcnews.com/business', + params={'formats': ['markdown', 'html']} + ) + end = time.time() + print(f"Time taken: {end - start} seconds") + print(len(scrape_status['markdown'])) + # save the markdown content with provider name + with open(f"{__data__}/firecrawl_simple.md", "w") as f: + f.write(scrape_status['markdown']) + # Count how many "cldnry.s-nbcnews.com" are in the markdown + print(scrape_status['markdown'].count("cldnry.s-nbcnews.com")) + + + + async with AsyncWebCrawler() as crawler: + start = time.time() + result = await crawler.arun( + url="https://www.nbcnews.com/business", + # js_code=["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"], + word_count_threshold=0, + bypass_cache=True, + verbose=False + ) + end = time.time() + print(f"Time taken: {end - start} seconds") + print(len(result.markdown)) + # save the markdown content with provider name + with open(f"{__data__}/crawl4ai_simple.md", "w") as f: + f.write(result.markdown) + # count how many "cldnry.s-nbcnews.com" are in the markdown + print(result.markdown.count("cldnry.s-nbcnews.com")) + + start = time.time() + result = await crawler.arun( + url="https://www.nbcnews.com/business", + js_code=["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"], + word_count_threshold=0, + bypass_cache=True, + verbose=False + ) + end = time.time() + print(f"Time taken: {end - start} seconds") + print(len(result.markdown)) + # save the markdown content with provider name + with open(f"{__data__}/crawl4ai_js.md", "w") as f: + f.write(result.markdown) + # count how many "cldnry.s-nbcnews.com" are in the markdown + print(result.markdown.count("cldnry.s-nbcnews.com")) + +if __name__ == "__main__": + asyncio.run(compare()) + \ No newline at end of file From 5c158376777d8c62df826f9d6ddc0e1f8d50eceb Mon Sep 17 00:00:00 2001 From: unclecode Date: Wed, 4 Sep 2024 14:46:22 +0800 Subject: [PATCH 11/57] chore: Update README, generate new notbook for quickstart --- README.md | 355 +++++++++++++++++--------- README.sync.md | 244 ++++++++++++++++++ crawl4ai/model_loader.py | 41 --- crawl4ai/onnx_embedding.py | 50 ---- docs/examples/quickstart.ipynb | 442 +++++++++++++++++++++++++++++++++ requirements.txt | 46 ++-- 6 files changed, 939 insertions(+), 239 deletions(-) create mode 100644 README.sync.md delete mode 100644 crawl4ai/onnx_embedding.py create mode 100644 docs/examples/quickstart.ipynb diff --git a/README.md b/README.md index 6bbef7e4..8d85d870 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Crawl4AI v0.2.77 🕷️🤖 +# Crawl4AI Async Version 🕷️🤖 [![GitHub Stars](https://img.shields.io/github/stars/unclecode/crawl4ai?style=social)](https://github.com/unclecode/crawl4ai/stargazers) [![GitHub Forks](https://img.shields.io/github/forks/unclecode/crawl4ai?style=social)](https://github.com/unclecode/crawl4ai/network/members) @@ -6,34 +6,22 @@ [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/unclecode/crawl4ai)](https://github.com/unclecode/crawl4ai/pulls) [![License](https://img.shields.io/github/license/unclecode/crawl4ai)](https://github.com/unclecode/crawl4ai/blob/main/LICENSE) -Crawl4AI simplifies web crawling and data extraction, making it accessible for large language models (LLMs) and AI applications. 🆓🌐 +Crawl4AI simplifies asynchronous web crawling and data extraction, making it accessible for large language models (LLMs) and AI applications. 🆓🌐 -#### [v0.2.77] - 2024-08-02 +> Looking for the synchronous version? Check out [README.sync.md](./README.sync.md). -Major improvements in functionality, performance, and cross-platform compatibility! 🚀 - -- 🐳 **Docker enhancements**: - - Significantly improved Dockerfile for easy installation on Linux, Mac, and Windows. -- 🌐 **Official Docker Hub image**: - - Launched our first official image on Docker Hub for streamlined deployment (unclecode/crawl4ai). -- 🔧 **Selenium upgrade**: - - Removed dependency on ChromeDriver, now using Selenium's built-in capabilities for better compatibility. -- 🖼️ **Image description**: - - Implemented ability to generate textual descriptions for extracted images from web pages. -- ⚡ **Performance boost**: - - Various improvements to enhance overall speed and performance. - ## Try it Now! ✨ Play around with this [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1sJPAmeLj5PMrg2VgOwMJ2ubGIcK0cJeX?usp=sharing) -✨ visit our [Documentation Website](https://crawl4ai.com/mkdocs/) +✨ Visit our [Documentation Website](https://crawl4ai.com/mkdocs/) -✨ Check [Demo](https://crawl4ai.com/mkdocs/demo) +✨ Check out the [Demo](https://crawl4ai.com/mkdocs/demo) ## Features ✨ - 🆓 Completely free and open-source +- 🚀 Blazing fast performance, outperforming many paid services - 🤖 LLM-friendly output formats (JSON, cleaned HTML, markdown) - 🌍 Supports crawling multiple URLs simultaneously - 🎨 Extracts and returns all media tags (Images, Audio, and Video) @@ -43,44 +31,17 @@ Major improvements in functionality, performance, and cross-platform compatibili - 🕵️ User-agent customization - 🖼️ Takes screenshots of the page - 📜 Executes multiple custom JavaScripts before crawling +- 📊 Generates structured output without LLM using JsonCssExtractionStrategy - 📚 Various chunking strategies: topic-based, regex, sentence, and more - 🧠 Advanced extraction strategies: cosine clustering, LLM, and more -- 🎯 CSS selector support +- 🎯 CSS selector support for precise data extraction - 📝 Passes instructions/keywords to refine extraction +- 🔒 Proxy support for enhanced privacy and access +- 🔄 Session management for complex multi-page crawling scenarios +- 🌐 Asynchronous architecture for improved performance and scalability -# Crawl4AI -## 🌟 Shoutout to Contributors of v0.2.77! - -A big thank you to the amazing contributors who've made this release possible: - -- [@aravindkarnam](https://github.com/aravindkarnam) for the new image description feature -- [@FractalMind](https://github.com/FractalMind) for our official Docker Hub image -- [@ketonkss4](https://github.com/ketonkss4) for helping streamline our Selenium setup - -Your contributions are driving Crawl4AI forward! 🚀 - -## Cool Examples 🚀 - -### Quick Start - -```python -from crawl4ai import WebCrawler - -# Create an instance of WebCrawler -crawler = WebCrawler() - -# Warm up the crawler (load necessary models) -crawler.warmup() - -# Run the crawler on a URL -result = crawler.run(url="https://www.nbcnews.com/business") - -# Print the extracted content -print(result.markdown) -``` - -## How to install 🛠 +## Installation 🛠️ ### Using pip 🐍 ```bash @@ -105,118 +66,264 @@ docker pull unclecode/crawl4ai:latest docker run -d -p 8000:80 unclecode/crawl4ai:latest ``` - -## Speed-First Design 🚀 - -Perhaps the most important design principle for this library is speed. We need to ensure it can handle many links and resources in parallel as quickly as possible. By combining this speed with fast LLMs like Groq, the results will be truly amazing. +## Quick Start 🚀 ```python -import time -from crawl4ai.web_crawler import WebCrawler -crawler = WebCrawler() -crawler.warmup() +import asyncio +from crawl4ai import AsyncWebCrawler -start = time.time() -url = r"https://www.nbcnews.com/business" -result = crawler.run( url, word_count_threshold=10, bypass_cache=True) -end = time.time() -print(f"Time taken: {end - start}") +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun(url="https://www.nbcnews.com/business") + print(result.markdown) + +if __name__ == "__main__": + asyncio.run(main()) ``` -Let's take a look the calculated time for the above code snippet: +## Advanced Usage 🔬 -```bash -[LOG] 🚀 Crawling done, success: True, time taken: 1.3623387813568115 seconds -[LOG] 🚀 Content extracted, success: True, time taken: 0.05715131759643555 seconds -[LOG] 🚀 Extraction, time taken: 0.05750393867492676 seconds. -Time taken: 1.439958095550537 +### Executing JavaScript and Using CSS Selectors + +```python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + js_code = ["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"] + result = await crawler.arun( + url="https://www.nbcnews.com/business", + js_code=js_code, + css_selector="article.tease-card", + bypass_cache=True + ) + print(result.extracted_content) + +if __name__ == "__main__": + asyncio.run(main()) ``` -Fetching the content from the page took 1.3623 seconds, and extracting the content took 0.0575 seconds. 🚀 -### Extract Structured Data from Web Pages 📊 +### Using a Proxy -Crawl all OpenAI models and their fees from the official page. +```python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def main(): + async with AsyncWebCrawler(verbose=True, proxy="http://127.0.0.1:7890") as crawler: + result = await crawler.arun( + url="https://www.nbcnews.com/business", + bypass_cache=True + ) + print(result.markdown) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +### Extracting Structured Data with OpenAI ```python import os -from crawl4ai import WebCrawler +import asyncio +from crawl4ai import AsyncWebCrawler from crawl4ai.extraction_strategy import LLMExtractionStrategy from pydantic import BaseModel, Field class OpenAIModelFee(BaseModel): model_name: str = Field(..., description="Name of the OpenAI model.") input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") - output_fee: str = Field(..., description="Fee for output token ßfor the OpenAI model.") + output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") -url = 'https://openai.com/api/pricing/' -crawler = WebCrawler() -crawler.warmup() +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url='https://openai.com/api/pricing/', + word_count_threshold=1, + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", api_token=os.getenv('OPENAI_API_KEY'), + schema=OpenAIModelFee.schema(), + extraction_type="schema", + instruction="""From the crawled content, extract all mentioned model names along with their fees for input and output tokens. + Do not miss any models in the entire content. One extracted model JSON format should look like this: + {"model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens"}.""" + ), + bypass_cache=True, + ) + print(result.extracted_content) -result = crawler.run( - url=url, - word_count_threshold=1, - extraction_strategy= LLMExtractionStrategy( - provider= "openai/gpt-4o", api_token = os.getenv('OPENAI_API_KEY'), - schema=OpenAIModelFee.schema(), - extraction_type="schema", - instruction="""From the crawled content, extract all mentioned model names along with their fees for input and output tokens. - Do not miss any models in the entire content. One extracted model JSON format should look like this: - {"model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens"}.""" - ), - bypass_cache=True, - ) - -print(result.extracted_content) +if __name__ == "__main__": + asyncio.run(main()) ``` -### Execute JS, Filter Data with CSS Selector, and Clustering +### Advanced Multi-Page Crawling with JavaScript Execution + +Crawl4AI excels at handling complex scenarios, such as crawling multiple pages with dynamic content loaded via JavaScript. Here's an example of crawling GitHub commits across multiple pages: ```python -from crawl4ai import WebCrawler -from crawl4ai.chunking_strategy import CosineStrategy +import asyncio +import re +from bs4 import BeautifulSoup +from crawl4ai import AsyncWebCrawler -js_code = ["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"] +async def crawl_typescript_commits(): + first_commit = "" + async def on_execution_started(page): + nonlocal first_commit + try: + while True: + await page.wait_for_selector('li.Box-sc-g0xbh4-0 h4') + commit = await page.query_selector('li.Box-sc-g0xbh4-0 h4') + commit = await commit.evaluate('(element) => element.textContent') + commit = re.sub(r'\s+', '', commit) + if commit and commit != first_commit: + first_commit = commit + break + await asyncio.sleep(0.5) + except Exception as e: + print(f"Warning: New content didn't appear after JavaScript execution: {e}") -crawler = WebCrawler() -crawler.warmup() + async with AsyncWebCrawler(verbose=True) as crawler: + crawler.crawler_strategy.set_hook('on_execution_started', on_execution_started) -result = crawler.run( - url="https://www.nbcnews.com/business", - js=js_code, - css_selector="p", - extraction_strategy=CosineStrategy(semantic_filter="technology") -) + url = "https://github.com/microsoft/TypeScript/commits/main" + session_id = "typescript_commits_session" + all_commits = [] -print(result.extracted_content) + js_next_page = """ + const button = document.querySelector('a[data-testid="pagination-next-button"]'); + if (button) button.click(); + """ + + for page in range(3): # Crawl 3 pages + result = await crawler.arun( + url=url, + session_id=session_id, + css_selector="li.Box-sc-g0xbh4-0", + js=js_next_page if page > 0 else None, + bypass_cache=True, + js_only=page > 0 + ) + + assert result.success, f"Failed to crawl page {page + 1}" + + soup = BeautifulSoup(result.cleaned_html, 'html.parser') + commits = soup.select("li") + all_commits.extend(commits) + + print(f"Page {page + 1}: Found {len(commits)} commits") + + await crawler.crawler_strategy.kill_session(session_id) + print(f"Successfully crawled {len(all_commits)} commits across 3 pages") + +if __name__ == "__main__": + asyncio.run(crawl_typescript_commits()) ``` -### Extract Structured Data from Web Pages With Proxy and BaseUrl +This example demonstrates Crawl4AI's ability to handle complex scenarios where content is loaded asynchronously. It crawls multiple pages of GitHub commits, executing JavaScript to load new content and using custom hooks to ensure data is loaded before proceeding. + +### Using JsonCssExtractionStrategy + +The `JsonCssExtractionStrategy` allows for precise extraction of structured data from web pages using CSS selectors. ```python -from crawl4ai import WebCrawler -from crawl4ai.extraction_strategy import LLMExtractionStrategy +import asyncio +import json +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy -def create_crawler(): - crawler = WebCrawler(verbose=True, proxy="http://127.0.0.1:7890") - crawler.warmup() - return crawler +async def extract_news_teasers(): + schema = { + "name": "News Teaser Extractor", + "baseSelector": ".wide-tease-item__wrapper", + "fields": [ + { + "name": "category", + "selector": ".unibrow span[data-testid='unibrow-text']", + "type": "text", + }, + { + "name": "headline", + "selector": ".wide-tease-item__headline", + "type": "text", + }, + { + "name": "summary", + "selector": ".wide-tease-item__description", + "type": "text", + }, + { + "name": "time", + "selector": "[data-testid='wide-tease-date']", + "type": "text", + }, + { + "name": "image", + "type": "nested", + "selector": "picture.teasePicture img", + "fields": [ + {"name": "src", "type": "attribute", "attribute": "src"}, + {"name": "alt", "type": "attribute", "attribute": "alt"}, + ], + }, + { + "name": "link", + "selector": "a[href]", + "type": "attribute", + "attribute": "href", + }, + ], + } -crawler = create_crawler() + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) -crawler.warmup() + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.nbcnews.com/business", + extraction_strategy=extraction_strategy, + bypass_cache=True, + ) -result = crawler.run( - url="https://www.nbcnews.com/business", - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token="sk-", - base_url="https://api.openai.com/v1" - ) -) + assert result.success, "Failed to crawl the page" -print(result.markdown) + news_teasers = json.loads(result.extracted_content) + print(f"Successfully extracted {len(news_teasers)} news teasers") + print(json.dumps(news_teasers[0], indent=2)) + +if __name__ == "__main__": + asyncio.run(extract_news_teasers()) ``` +## Speed Comparison 🚀 + +Crawl4AI is designed with speed as a primary focus. Our goal is to provide the fastest possible response with high-quality data extraction, minimizing abstractions between the data and the user. + +We've conducted a speed comparison between Crawl4AI and Firecrawl, a paid service. The results demonstrate Crawl4AI's superior performance: + +``` +Firecrawl: +Time taken: 7.02 seconds +Content length: 42074 characters +Images found: 49 + +Crawl4AI (simple crawl): +Time taken: 1.60 seconds +Content length: 18238 characters +Images found: 49 + +Crawl4AI (with JavaScript execution): +Time taken: 4.64 seconds +Content length: 40869 characters +Images found: 89 +``` + +As you can see, Crawl4AI outperforms Firecrawl significantly: +- Simple crawl: Crawl4AI is over 4 times faster than Firecrawl. +- With JavaScript execution: Even when executing JavaScript to load more content (doubling the number of images found), Crawl4AI is still faster than Firecrawl's simple crawl. + +You can find the full comparison code in our repository at `docs/examples/crawl4ai_vs_firecrawl.py`. + ## Documentation 📚 For detailed documentation, including installation instructions, advanced features, and API reference, visit our [Documentation Website](https://crawl4ai.com/mkdocs/). diff --git a/README.sync.md b/README.sync.md new file mode 100644 index 00000000..6bbef7e4 --- /dev/null +++ b/README.sync.md @@ -0,0 +1,244 @@ +# Crawl4AI v0.2.77 🕷️🤖 + +[![GitHub Stars](https://img.shields.io/github/stars/unclecode/crawl4ai?style=social)](https://github.com/unclecode/crawl4ai/stargazers) +[![GitHub Forks](https://img.shields.io/github/forks/unclecode/crawl4ai?style=social)](https://github.com/unclecode/crawl4ai/network/members) +[![GitHub Issues](https://img.shields.io/github/issues/unclecode/crawl4ai)](https://github.com/unclecode/crawl4ai/issues) +[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/unclecode/crawl4ai)](https://github.com/unclecode/crawl4ai/pulls) +[![License](https://img.shields.io/github/license/unclecode/crawl4ai)](https://github.com/unclecode/crawl4ai/blob/main/LICENSE) + +Crawl4AI simplifies web crawling and data extraction, making it accessible for large language models (LLMs) and AI applications. 🆓🌐 + +#### [v0.2.77] - 2024-08-02 + +Major improvements in functionality, performance, and cross-platform compatibility! 🚀 + +- 🐳 **Docker enhancements**: + - Significantly improved Dockerfile for easy installation on Linux, Mac, and Windows. +- 🌐 **Official Docker Hub image**: + - Launched our first official image on Docker Hub for streamlined deployment (unclecode/crawl4ai). +- 🔧 **Selenium upgrade**: + - Removed dependency on ChromeDriver, now using Selenium's built-in capabilities for better compatibility. +- 🖼️ **Image description**: + - Implemented ability to generate textual descriptions for extracted images from web pages. +- ⚡ **Performance boost**: + - Various improvements to enhance overall speed and performance. + +## Try it Now! + +✨ Play around with this [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1sJPAmeLj5PMrg2VgOwMJ2ubGIcK0cJeX?usp=sharing) + +✨ visit our [Documentation Website](https://crawl4ai.com/mkdocs/) + +✨ Check [Demo](https://crawl4ai.com/mkdocs/demo) + +## Features ✨ + +- 🆓 Completely free and open-source +- 🤖 LLM-friendly output formats (JSON, cleaned HTML, markdown) +- 🌍 Supports crawling multiple URLs simultaneously +- 🎨 Extracts and returns all media tags (Images, Audio, and Video) +- 🔗 Extracts all external and internal links +- 📚 Extracts metadata from the page +- 🔄 Custom hooks for authentication, headers, and page modifications before crawling +- 🕵️ User-agent customization +- 🖼️ Takes screenshots of the page +- 📜 Executes multiple custom JavaScripts before crawling +- 📚 Various chunking strategies: topic-based, regex, sentence, and more +- 🧠 Advanced extraction strategies: cosine clustering, LLM, and more +- 🎯 CSS selector support +- 📝 Passes instructions/keywords to refine extraction + +# Crawl4AI + +## 🌟 Shoutout to Contributors of v0.2.77! + +A big thank you to the amazing contributors who've made this release possible: + +- [@aravindkarnam](https://github.com/aravindkarnam) for the new image description feature +- [@FractalMind](https://github.com/FractalMind) for our official Docker Hub image +- [@ketonkss4](https://github.com/ketonkss4) for helping streamline our Selenium setup + +Your contributions are driving Crawl4AI forward! 🚀 + +## Cool Examples 🚀 + +### Quick Start + +```python +from crawl4ai import WebCrawler + +# Create an instance of WebCrawler +crawler = WebCrawler() + +# Warm up the crawler (load necessary models) +crawler.warmup() + +# Run the crawler on a URL +result = crawler.run(url="https://www.nbcnews.com/business") + +# Print the extracted content +print(result.markdown) +``` + +## How to install 🛠 + +### Using pip 🐍 +```bash +virtualenv venv +source venv/bin/activate +pip install "crawl4ai @ git+https://github.com/unclecode/crawl4ai.git" +``` + +### Using Docker 🐳 + +```bash +# For Mac users (M1/M2) +# docker build --platform linux/amd64 -t crawl4ai . +docker build -t crawl4ai . +docker run -d -p 8000:80 crawl4ai +``` + +### Using Docker Hub 🐳 + +```bash +docker pull unclecode/crawl4ai:latest +docker run -d -p 8000:80 unclecode/crawl4ai:latest +``` + + +## Speed-First Design 🚀 + +Perhaps the most important design principle for this library is speed. We need to ensure it can handle many links and resources in parallel as quickly as possible. By combining this speed with fast LLMs like Groq, the results will be truly amazing. + +```python +import time +from crawl4ai.web_crawler import WebCrawler +crawler = WebCrawler() +crawler.warmup() + +start = time.time() +url = r"https://www.nbcnews.com/business" +result = crawler.run( url, word_count_threshold=10, bypass_cache=True) +end = time.time() +print(f"Time taken: {end - start}") +``` + +Let's take a look the calculated time for the above code snippet: + +```bash +[LOG] 🚀 Crawling done, success: True, time taken: 1.3623387813568115 seconds +[LOG] 🚀 Content extracted, success: True, time taken: 0.05715131759643555 seconds +[LOG] 🚀 Extraction, time taken: 0.05750393867492676 seconds. +Time taken: 1.439958095550537 +``` +Fetching the content from the page took 1.3623 seconds, and extracting the content took 0.0575 seconds. 🚀 + +### Extract Structured Data from Web Pages 📊 + +Crawl all OpenAI models and their fees from the official page. + +```python +import os +from crawl4ai import WebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy +from pydantic import BaseModel, Field + +class OpenAIModelFee(BaseModel): + model_name: str = Field(..., description="Name of the OpenAI model.") + input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") + output_fee: str = Field(..., description="Fee for output token ßfor the OpenAI model.") + +url = 'https://openai.com/api/pricing/' +crawler = WebCrawler() +crawler.warmup() + +result = crawler.run( + url=url, + word_count_threshold=1, + extraction_strategy= LLMExtractionStrategy( + provider= "openai/gpt-4o", api_token = os.getenv('OPENAI_API_KEY'), + schema=OpenAIModelFee.schema(), + extraction_type="schema", + instruction="""From the crawled content, extract all mentioned model names along with their fees for input and output tokens. + Do not miss any models in the entire content. One extracted model JSON format should look like this: + {"model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens"}.""" + ), + bypass_cache=True, + ) + +print(result.extracted_content) +``` + +### Execute JS, Filter Data with CSS Selector, and Clustering + +```python +from crawl4ai import WebCrawler +from crawl4ai.chunking_strategy import CosineStrategy + +js_code = ["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"] + +crawler = WebCrawler() +crawler.warmup() + +result = crawler.run( + url="https://www.nbcnews.com/business", + js=js_code, + css_selector="p", + extraction_strategy=CosineStrategy(semantic_filter="technology") +) + +print(result.extracted_content) +``` + +### Extract Structured Data from Web Pages With Proxy and BaseUrl + +```python +from crawl4ai import WebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +def create_crawler(): + crawler = WebCrawler(verbose=True, proxy="http://127.0.0.1:7890") + crawler.warmup() + return crawler + +crawler = create_crawler() + +crawler.warmup() + +result = crawler.run( + url="https://www.nbcnews.com/business", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token="sk-", + base_url="https://api.openai.com/v1" + ) +) + +print(result.markdown) +``` + +## Documentation 📚 + +For detailed documentation, including installation instructions, advanced features, and API reference, visit our [Documentation Website](https://crawl4ai.com/mkdocs/). + +## Contributing 🤝 + +We welcome contributions from the open-source community. Check out our [contribution guidelines](https://github.com/unclecode/crawl4ai/blob/main/CONTRIBUTING.md) for more information. + +## License 📄 + +Crawl4AI is released under the [Apache 2.0 License](https://github.com/unclecode/crawl4ai/blob/main/LICENSE). + +## Contact 📧 + +For questions, suggestions, or feedback, feel free to reach out: + +- GitHub: [unclecode](https://github.com/unclecode) +- Twitter: [@unclecode](https://twitter.com/unclecode) +- Website: [crawl4ai.com](https://crawl4ai.com) + +Happy Crawling! 🕸️🚀 + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=unclecode/crawl4ai&type=Date)](https://star-history.com/#unclecode/crawl4ai&Date) \ No newline at end of file diff --git a/crawl4ai/model_loader.py b/crawl4ai/model_loader.py index f49a0659..7608ca51 100644 --- a/crawl4ai/model_loader.py +++ b/crawl4ai/model_loader.py @@ -80,47 +80,6 @@ def load_bge_small_en_v1_5(): model, device = set_model_device(model) return tokenizer, model -@lru_cache() -def load_onnx_all_MiniLM_l6_v2(): - from crawl4ai.onnx_embedding import DefaultEmbeddingModel - - model_path = "models/onnx.tar.gz" - model_url = "https://unclecode-files.s3.us-west-2.amazonaws.com/onnx.tar.gz" - __location__ = os.path.realpath( - os.path.join(os.getcwd(), os.path.dirname(__file__))) - download_path = os.path.join(__location__, model_path) - onnx_dir = os.path.join(__location__, "models/onnx") - - # Create the models directory if it does not exist - os.makedirs(os.path.dirname(download_path), exist_ok=True) - - # Download the tar.gz file if it does not exist - if not os.path.exists(download_path): - def download_with_progress(url, filename): - def reporthook(block_num, block_size, total_size): - downloaded = block_num * block_size - percentage = 100 * downloaded / total_size - if downloaded < total_size: - print(f"\rDownloading: {percentage:.2f}% ({downloaded / (1024 * 1024):.2f} MB of {total_size / (1024 * 1024):.2f} MB)", end='') - else: - print("\rDownload complete!") - - urllib.request.urlretrieve(url, filename, reporthook) - - download_with_progress(model_url, download_path) - - # Extract the tar.gz file if the onnx directory does not exist - if not os.path.exists(onnx_dir): - with tarfile.open(download_path, "r:gz") as tar: - tar.extractall(path=os.path.join(__location__, "models")) - - # remove the tar.gz file - os.remove(download_path) - - - - model = DefaultEmbeddingModel() - return model @lru_cache() def load_text_classifier(): diff --git a/crawl4ai/onnx_embedding.py b/crawl4ai/onnx_embedding.py deleted file mode 100644 index af5e5f20..00000000 --- a/crawl4ai/onnx_embedding.py +++ /dev/null @@ -1,50 +0,0 @@ -# A dependency-light way to run the onnx model - - -import numpy as np -from typing import List -import os - -__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) -MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2" - -def normalize(v): - norm = np.linalg.norm(v, axis=1) - norm[norm == 0] = 1e-12 - return v / norm[:, np.newaxis] - -# Sampel implementation of the default sentence-transformers model using ONNX -class DefaultEmbeddingModel(): - - def __init__(self): - from tokenizers import Tokenizer - import onnxruntime as ort - # max_seq_length = 256, for some reason sentence-transformers uses 256 even though the HF config has a max length of 128 - # https://github.com/UKPLab/sentence-transformers/blob/3e1929fddef16df94f8bc6e3b10598a98f46e62d/docs/_static/html/models_en_sentence_embeddings.html#LL480 - self.tokenizer = Tokenizer.from_file(os.path.join(__location__, "models/onnx/tokenizer.json")) - self.tokenizer.enable_truncation(max_length=256) - self.tokenizer.enable_padding(pad_id=0, pad_token="[PAD]", length=256) - self.model = ort.InferenceSession(os.path.join(__location__,"models/onnx/model.onnx")) - - - def __call__(self, documents: List[str], batch_size: int = 32): - all_embeddings = [] - for i in range(0, len(documents), batch_size): - batch = documents[i:i + batch_size] - encoded = [self.tokenizer.encode(d) for d in batch] - input_ids = np.array([e.ids for e in encoded]) - attention_mask = np.array([e.attention_mask for e in encoded]) - onnx_input = { - "input_ids": np.array(input_ids, dtype=np.int64), - "attention_mask": np.array(attention_mask, dtype=np.int64), - "token_type_ids": np.array([np.zeros(len(e), dtype=np.int64) for e in input_ids], dtype=np.int64), - } - model_output = self.model.run(None, onnx_input) - last_hidden_state = model_output[0] - # Perform mean pooling with attention weighting - input_mask_expanded = np.broadcast_to(np.expand_dims(attention_mask, -1), last_hidden_state.shape) - embeddings = np.sum(last_hidden_state * input_mask_expanded, 1) / np.clip(input_mask_expanded.sum(1), a_min=1e-9, a_max=None) - embeddings = normalize(embeddings).astype(np.float32) - all_embeddings.append(embeddings) - return np.concatenate(all_embeddings) - diff --git a/docs/examples/quickstart.ipynb b/docs/examples/quickstart.ipynb new file mode 100644 index 00000000..738b21b4 --- /dev/null +++ b/docs/examples/quickstart.ipynb @@ -0,0 +1,442 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Crawl4AI: Advanced Web Crawling and Data Extraction\n", + "\n", + "Welcome to this interactive notebook showcasing Crawl4AI, an advanced asynchronous web crawling and data extraction library.\n", + "\n", + "- GitHub Repository: [https://github.com/unclecode/crawl4ai](https://github.com/unclecode/crawl4ai)\n", + "- Twitter: [@unclecode](https://twitter.com/unclecode)\n", + "- Website: [https://crawl4ai.com](https://crawl4ai.com)\n", + "\n", + "Let's explore the powerful features of Crawl4AI!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Installation\n", + "\n", + "First, let's install Crawl4AI from GitHub:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install \"crawl4ai @ git+https://github.com/unclecode/crawl4ai.git\"\n", + "!pip install nest-asyncio\n", + "!playwright install" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's import the necessary libraries:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "import nest_asyncio\n", + "from crawl4ai import AsyncWebCrawler\n", + "from crawl4ai.extraction_strategy import JsonCssExtractionStrategy, LLMExtractionStrategy\n", + "import json\n", + "import time\n", + "from pydantic import BaseModel, Field\n", + "\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic Usage\n", + "\n", + "Let's start with a simple crawl example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def simple_crawl():\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " result = await crawler.arun(url=\"https://www.nbcnews.com/business\")\n", + " print(result.markdown[:500]) # Print first 500 characters\n", + "\n", + "await simple_crawl()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced Features\n", + "\n", + "### Executing JavaScript and Using CSS Selectors" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def js_and_css():\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " js_code = [\"const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();\"]\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " js_code=js_code,\n", + " css_selector=\"article.tease-card\",\n", + " bypass_cache=True\n", + " )\n", + " print(result.extracted_content[:500]) # Print first 500 characters\n", + "\n", + "await js_and_css()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using a Proxy\n", + "\n", + "Note: You'll need to replace the proxy URL with a working proxy for this example to run successfully." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def use_proxy():\n", + " async with AsyncWebCrawler(verbose=True, proxy=\"http://your-proxy-url:port\") as crawler:\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " bypass_cache=True\n", + " )\n", + " print(result.markdown[:500]) # Print first 500 characters\n", + "\n", + "# Uncomment the following line to run the proxy example\n", + "# await use_proxy()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extracting Structured Data with OpenAI\n", + "\n", + "Note: You'll need to set your OpenAI API key as an environment variable for this example to work." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "class OpenAIModelFee(BaseModel):\n", + " model_name: str = Field(..., description=\"Name of the OpenAI model.\")\n", + " input_fee: str = Field(..., description=\"Fee for input token for the OpenAI model.\")\n", + " output_fee: str = Field(..., description=\"Fee for output token for the OpenAI model.\")\n", + "\n", + "async def extract_openai_fees():\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " result = await crawler.arun(\n", + " url='https://openai.com/api/pricing/',\n", + " word_count_threshold=1,\n", + " extraction_strategy=LLMExtractionStrategy(\n", + " provider=\"openai/gpt-4o\", api_token=os.getenv('OPENAI_API_KEY'), \n", + " schema=OpenAIModelFee.schema(),\n", + " extraction_type=\"schema\",\n", + " instruction=\"\"\"From the crawled content, extract all mentioned model names along with their fees for input and output tokens. \n", + " Do not miss any models in the entire content. One extracted model JSON format should look like this: \n", + " {\"model_name\": \"GPT-4\", \"input_fee\": \"US$10.00 / 1M tokens\", \"output_fee\": \"US$30.00 / 1M tokens\"}.\"\"\"\n", + " ), \n", + " bypass_cache=True,\n", + " )\n", + " print(result.extracted_content)\n", + "\n", + "# Uncomment the following line to run the OpenAI extraction example\n", + "# await extract_openai_fees()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Advanced Multi-Page Crawling with JavaScript Execution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import re\n", + "from bs4 import BeautifulSoup\n", + "\n", + "async def crawl_typescript_commits():\n", + " first_commit = \"\"\n", + " async def on_execution_started(page):\n", + " nonlocal first_commit \n", + " try:\n", + " while True:\n", + " await page.wait_for_selector('li.Box-sc-g0xbh4-0 h4')\n", + " commit = await page.query_selector('li.Box-sc-g0xbh4-0 h4')\n", + " commit = await commit.evaluate('(element) => element.textContent')\n", + " commit = re.sub(r'\\s+', '', commit)\n", + " if commit and commit != first_commit:\n", + " first_commit = commit\n", + " break\n", + " await asyncio.sleep(0.5)\n", + " except Exception as e:\n", + " print(f\"Warning: New content didn't appear after JavaScript execution: {e}\")\n", + "\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " crawler.crawler_strategy.set_hook('on_execution_started', on_execution_started)\n", + "\n", + " url = \"https://github.com/microsoft/TypeScript/commits/main\"\n", + " session_id = \"typescript_commits_session\"\n", + " all_commits = []\n", + "\n", + " js_next_page = \"\"\"\n", + " const button = document.querySelector('a[data-testid=\"pagination-next-button\"]');\n", + " if (button) button.click();\n", + " \"\"\"\n", + "\n", + " for page in range(3): # Crawl 3 pages\n", + " result = await crawler.arun(\n", + " url=url,\n", + " session_id=session_id,\n", + " css_selector=\"li.Box-sc-g0xbh4-0\",\n", + " js=js_next_page if page > 0 else None,\n", + " bypass_cache=True,\n", + " js_only=page > 0\n", + " )\n", + "\n", + " assert result.success, f\"Failed to crawl page {page + 1}\"\n", + "\n", + " soup = BeautifulSoup(result.cleaned_html, 'html.parser')\n", + " commits = soup.select(\"li\")\n", + " all_commits.extend(commits)\n", + "\n", + " print(f\"Page {page + 1}: Found {len(commits)} commits\")\n", + "\n", + " await crawler.crawler_strategy.kill_session(session_id)\n", + " print(f\"Successfully crawled {len(all_commits)} commits across 3 pages\")\n", + "\n", + "await crawl_typescript_commits()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using JsonCssExtractionStrategy for Fast Structured Output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def extract_news_teasers():\n", + " schema = {\n", + " \"name\": \"News Teaser Extractor\",\n", + " \"baseSelector\": \".wide-tease-item__wrapper\",\n", + " \"fields\": [\n", + " {\n", + " \"name\": \"category\",\n", + " \"selector\": \".unibrow span[data-testid='unibrow-text']\",\n", + " \"type\": \"text\",\n", + " },\n", + " {\n", + " \"name\": \"headline\",\n", + " \"selector\": \".wide-tease-item__headline\",\n", + " \"type\": \"text\",\n", + " },\n", + " {\n", + " \"name\": \"summary\",\n", + " \"selector\": \".wide-tease-item__description\",\n", + " \"type\": \"text\",\n", + " },\n", + " {\n", + " \"name\": \"time\",\n", + " \"selector\": \"[data-testid='wide-tease-date']\",\n", + " \"type\": \"text\",\n", + " },\n", + " {\n", + " \"name\": \"image\",\n", + " \"type\": \"nested\",\n", + " \"selector\": \"picture.teasePicture img\",\n", + " \"fields\": [\n", + " {\"name\": \"src\", \"type\": \"attribute\", \"attribute\": \"src\"},\n", + " {\"name\": \"alt\", \"type\": \"attribute\", \"attribute\": \"alt\"},\n", + " ],\n", + " },\n", + " {\n", + " \"name\": \"link\",\n", + " \"selector\": \"a[href]\",\n", + " \"type\": \"attribute\",\n", + " \"attribute\": \"href\",\n", + " },\n", + " ],\n", + " }\n", + "\n", + " extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True)\n", + "\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " extraction_strategy=extraction_strategy,\n", + " bypass_cache=True,\n", + " )\n", + "\n", + " assert result.success, \"Failed to crawl the page\"\n", + "\n", + " news_teasers = json.loads(result.extracted_content)\n", + " print(f\"Successfully extracted {len(news_teasers)} news teasers\")\n", + " print(json.dumps(news_teasers[0], indent=2))\n", + "\n", + "await extract_news_teasers()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Speed Comparison\n", + "\n", + "Let's compare the speed of Crawl4AI with Firecrawl, a paid service. Note that we can't run Firecrawl in this Colab environment, so we'll simulate its performance based on previously recorded data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "async def speed_comparison():\n", + " # Simulated Firecrawl performance\n", + " print(\"Firecrawl (simulated):\")\n", + " print(\"Time taken: 7.02 seconds\")\n", + " print(\"Content length: 42074 characters\")\n", + " print(\"Images found: 49\")\n", + " print()\n", + "\n", + " async with AsyncWebCrawler() as crawler:\n", + " # Crawl4AI simple crawl\n", + " start = time.time()\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " word_count_threshold=0,\n", + " bypass_cache=True, \n", + " verbose=False\n", + " )\n", + " end = time.time()\n", + " print(\"Crawl4AI (simple crawl):\")\n", + " print(f\"Time taken: {end - start:.2f} seconds\")\n", + " print(f\"Content length: {len(result.markdown)} characters\")\n", + " print(f\"Images found: {result.markdown.count('cldnry.s-nbcnews.com')}\")\n", + " print()\n", + "\n", + " # Crawl4AI with JavaScript execution\n", + " start = time.time()\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " js_code=[\"const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();\"],\n", + " word_count_threshold=0,\n", + " bypass_cache=True, \n", + " verbose=False\n", + " )\n", + " end = time.time()\n", + " print(\"Crawl4AI (with JavaScript execution):\")\n", + " print(f\"Time taken: {end - start:.2f} seconds\")\n", + " print(f\"Content length: {len(result.markdown)} characters\")\n", + " print(f\"Images found: {result.markdown.count('cldnry.s-nbcnews.com')}\")\n", + "\n", + "await speed_comparison()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, Crawl4AI outperforms Firecrawl significantly:\n", + "- Simple crawl: Crawl4AI is typically over 4 times faster than Firecrawl.\n", + "- With JavaScript execution: Even when executing JavaScript to load more content (potentially doubling the number of images found), Crawl4AI is still faster than Firecrawl's simple crawl.\n", + "\n", + "Please note that actual performance may vary depending on network conditions and the specific content being crawled." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "In this notebook, we've explored the powerful features of Crawl4AI, including:\n", + "\n", + "1. Basic crawling\n", + "2. JavaScript execution and CSS selector usage\n", + "3. Proxy support\n", + "4. Structured data extraction with OpenAI\n", + "5. Advanced multi-page crawling with JavaScript execution\n", + "6. Fast structured output using JsonCssExtractionStrategy\n", + "7. Speed comparison with other services\n", + "\n", + "Crawl4AI offers a fast, flexible, and powerful solution for web crawling and data extraction tasks. Its asynchronous architecture and advanced features make it suitable for a wide range of applications, from simple web scraping to complex, multi-page data extraction scenarios.\n", + "\n", + "For more information and advanced usage, please visit the [Crawl4AI documentation](https://crawl4ai.com/mkdocs/).\n", + "\n", + "Happy crawling!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/requirements.txt b/requirements.txt index 2574cf60..772acd7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,22 @@ -numpy==1.25.0 -aiohttp==3.9.5 -aiosqlite==0.20.0 -beautifulsoup4==4.12.3 -fastapi==0.111.0 -html2text==2024.2.26 -httpx==0.27.0 -litellm==1.40.17 -nltk==3.8.1 -pydantic==2.7.4 -python-dotenv==1.0.1 -requests==2.32.3 -rich==13.7.1 -scikit-learn==1.5.0 -selenium==4.23.1 -uvicorn==0.30.1 -transformers==4.41.2 -# webdriver-manager==4.0.1 -# chromedriver-autoinstaller==0.6.4 -torch==2.3.1 -onnxruntime==1.18.0 -tokenizers==0.19.1 -pillow==10.3.0 -slowapi==0.1.9 \ No newline at end of file +numpy>=1.25.0 +aiohttp>=3.9.5 +aiosqlite>=0.20.0 +beautifulsoup4>=4.12.3 +fastapi>=0.111.0 +html2text>=2024.2.26 +httpx>=0.27.0 +litellm>=1.40.17 +nltk>=3.8.1 +pydantic>=2.7.4 +python-dotenv>=1.0.1 +requests>=2.32.3 +rich>=13.7.1 +scikit-learn>=1.5.0 +selenium>=4.23.1 +uvicorn>=0.30.1 +transformers>=4.41.2 +torch>=2.3.1 +tokenizers>=0.19.1 +pillow>=10.3.0 +slowapi>=0.1.9 +playwright>=1.46.0 \ No newline at end of file From eb131bebdf1ba8abc68e0f1f810d186048926d99 Mon Sep 17 00:00:00 2001 From: unclecode Date: Wed, 4 Sep 2024 15:33:24 +0800 Subject: [PATCH 12/57] Create series of quickstart files. --- README.md | 2 +- docs/examples/quickstart.ipynb | 1174 +++++++++++------ docs/examples/quickstart_async.py | 242 ++++ .../{quickstart.py => quickstart_sync.py} | 0 4 files changed, 977 insertions(+), 441 deletions(-) create mode 100644 docs/examples/quickstart_async.py rename docs/examples/{quickstart.py => quickstart_sync.py} (100%) diff --git a/README.md b/README.md index 8d85d870..a1c2c449 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Crawl4AI simplifies asynchronous web crawling and data extraction, making it acc ## Try it Now! -✨ Play around with this [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1sJPAmeLj5PMrg2VgOwMJ2ubGIcK0cJeX?usp=sharing) +✨ Play around with this [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1REChY6fXQf-EaVYLv0eHEWvzlYxGm0pd?usp=sharing) ✨ Visit our [Documentation Website](https://crawl4ai.com/mkdocs/) diff --git a/docs/examples/quickstart.ipynb b/docs/examples/quickstart.ipynb index 738b21b4..82694847 100644 --- a/docs/examples/quickstart.ipynb +++ b/docs/examples/quickstart.ipynb @@ -1,442 +1,736 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Crawl4AI: Advanced Web Crawling and Data Extraction\n", - "\n", - "Welcome to this interactive notebook showcasing Crawl4AI, an advanced asynchronous web crawling and data extraction library.\n", - "\n", - "- GitHub Repository: [https://github.com/unclecode/crawl4ai](https://github.com/unclecode/crawl4ai)\n", - "- Twitter: [@unclecode](https://twitter.com/unclecode)\n", - "- Website: [https://crawl4ai.com](https://crawl4ai.com)\n", - "\n", - "Let's explore the powerful features of Crawl4AI!" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "6yLvrXn7yZQI" + }, + "source": [ + "# Crawl4AI: Advanced Web Crawling and Data Extraction\n", + "\n", + "Welcome to this interactive notebook showcasing Crawl4AI, an advanced asynchronous web crawling and data extraction library.\n", + "\n", + "- GitHub Repository: [https://github.com/unclecode/crawl4ai](https://github.com/unclecode/crawl4ai)\n", + "- Twitter: [@unclecode](https://twitter.com/unclecode)\n", + "- Website: [https://crawl4ai.com](https://crawl4ai.com)\n", + "\n", + "Let's explore the powerful features of Crawl4AI!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KIn_9nxFyZQK" + }, + "source": [ + "## Installation\n", + "\n", + "First, let's install Crawl4AI from GitHub:" + ] + }, + { + "cell_type": "code", + "source": [ + "!sudo apt-get update && sudo apt-get install -y libwoff1 libopus0 libwebp6 libwebpdemux2 libenchant1c2a libgudev-1.0-0 libsecret-1-0 libhyphen0 libgdk-pixbuf2.0-0 libegl1 libnotify4 libxslt1.1 libevent-2.1-7 libgles2 libvpx6 libxcomposite1 libatk1.0-0 libatk-bridge2.0-0 libepoxy0 libgtk-3-0 libharfbuzz-icu0" + ], + "metadata": { + "id": "mSnaxLf3zMog" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xlXqaRtayZQK" + }, + "outputs": [], + "source": [ + "# !pip install \"crawl4ai @ git+https://github.com/unclecode/crawl4ai.git\"\n", + "!pip install \"crawl4ai @ git+https://github.com/unclecode/crawl4ai.git@staging\"\n", + "!pip install nest-asyncio\n", + "!playwright install" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qKCE7TI7yZQL" + }, + "source": [ + "Now, let's import the necessary libraries:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "I67tr7aAyZQL" + }, + "outputs": [], + "source": [ + "import asyncio\n", + "import nest_asyncio\n", + "from crawl4ai import AsyncWebCrawler\n", + "from crawl4ai.extraction_strategy import JsonCssExtractionStrategy, LLMExtractionStrategy\n", + "import json\n", + "import time\n", + "from pydantic import BaseModel, Field\n", + "\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h7yR_Rt_yZQM" + }, + "source": [ + "## Basic Usage\n", + "\n", + "Let's start with a simple crawl example:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "yBh6hf4WyZQM", + "outputId": "0f83af5c-abba-4175-ed95-70b7512e6bcc" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[LOG] 🌤️ Warming up the AsyncWebCrawler\n", + "[LOG] 🌞 AsyncWebCrawler is ready to crawl\n", + "[LOG] 🚀 Content extracted for https://www.nbcnews.com/business, success: True, time taken: 0.18 seconds\n", + "[LOG] 🚀 Extraction done for https://www.nbcnews.com/business, time taken: 0.18 seconds.\n", + "18219\n" + ] + } + ], + "source": [ + "async def simple_crawl():\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " result = await crawler.arun(url=\"https://www.nbcnews.com/business\")\n", + " print(len(result.markdown))\n", + "await simple_crawl()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "💡 By default, **Crawl4AI** caches the result of every URL, so the next time you call it, you’ll get an instant result. But if you want to bypass the cache, just set `bypass_cache=True`." + ], + "metadata": { + "id": "9rtkgHI28uI4" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MzZ0zlJ9yZQM" + }, + "source": [ + "## Advanced Features\n", + "\n", + "### Executing JavaScript and Using CSS Selectors" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "gHStF86xyZQM", + "outputId": "34d0fb6d-4dec-4677-f76e-85a1f082829b" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[LOG] 🌤️ Warming up the AsyncWebCrawler\n", + "[LOG] 🌞 AsyncWebCrawler is ready to crawl\n", + "[LOG] 🕸️ Crawling https://www.nbcnews.com/business using AsyncPlaywrightCrawlerStrategy...\n", + "[LOG] ✅ Crawled https://www.nbcnews.com/business successfully!\n", + "[LOG] 🚀 Crawling done for https://www.nbcnews.com/business, success: True, time taken: 9.78 seconds\n", + "[LOG] 🚀 Content extracted for https://www.nbcnews.com/business, success: True, time taken: 0.44 seconds\n", + "[LOG] 🔥 Extracting semantic blocks for https://www.nbcnews.com/business, Strategy: AsyncWebCrawler\n", + "[LOG] 🚀 Extraction done for https://www.nbcnews.com/business, time taken: 0.45 seconds.\n", + "34241\n" + ] + } + ], + "source": [ + "async def js_and_css():\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " js_code = [\"const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();\"]\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " js_code=js_code,\n", + " # css_selector=\"YOUR_CSS_SELECTOR_HERE\",\n", + " bypass_cache=True\n", + " )\n", + " print(len(result.markdown))\n", + "\n", + "await js_and_css()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cqE_W4coyZQM" + }, + "source": [ + "### Using a Proxy\n", + "\n", + "Note: You'll need to replace the proxy URL with a working proxy for this example to run successfully." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QjAyiAGqyZQM" + }, + "outputs": [], + "source": [ + "async def use_proxy():\n", + " async with AsyncWebCrawler(verbose=True, proxy=\"http://your-proxy-url:port\") as crawler:\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " bypass_cache=True\n", + " )\n", + " print(result.markdown[:500]) # Print first 500 characters\n", + "\n", + "# Uncomment the following line to run the proxy example\n", + "# await use_proxy()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XTZ88lbayZQN" + }, + "source": [ + "### Extracting Structured Data with OpenAI\n", + "\n", + "Note: You'll need to set your OpenAI API key as an environment variable for this example to work." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "fIOlDayYyZQN", + "outputId": "cb8359cc-dee0-4762-9698-5dfdcee055b8" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[LOG] 🌤️ Warming up the AsyncWebCrawler\n", + "[LOG] 🌞 AsyncWebCrawler is ready to crawl\n", + "[LOG] 🕸️ Crawling https://openai.com/api/pricing/ using AsyncPlaywrightCrawlerStrategy...\n", + "[LOG] ✅ Crawled https://openai.com/api/pricing/ successfully!\n", + "[LOG] 🚀 Crawling done for https://openai.com/api/pricing/, success: True, time taken: 3.77 seconds\n", + "[LOG] 🚀 Content extracted for https://openai.com/api/pricing/, success: True, time taken: 0.21 seconds\n", + "[LOG] 🔥 Extracting semantic blocks for https://openai.com/api/pricing/, Strategy: AsyncWebCrawler\n", + "[LOG] Call LLM for https://openai.com/api/pricing/ - block index: 0\n", + "[LOG] Call LLM for https://openai.com/api/pricing/ - block index: 1\n", + "[LOG] Call LLM for https://openai.com/api/pricing/ - block index: 2\n", + "[LOG] Call LLM for https://openai.com/api/pricing/ - block index: 3\n", + "[LOG] Extracted 4 blocks from URL: https://openai.com/api/pricing/ block index: 3\n", + "[LOG] Call LLM for https://openai.com/api/pricing/ - block index: 4\n", + "[LOG] Extracted 5 blocks from URL: https://openai.com/api/pricing/ block index: 0\n", + "[LOG] Extracted 1 blocks from URL: https://openai.com/api/pricing/ block index: 4\n", + "[LOG] Extracted 8 blocks from URL: https://openai.com/api/pricing/ block index: 1\n", + "[LOG] Extracted 12 blocks from URL: https://openai.com/api/pricing/ block index: 2\n", + "[LOG] 🚀 Extraction done for https://openai.com/api/pricing/, time taken: 8.55 seconds.\n", + "5029\n" + ] + } + ], + "source": [ + "import os\n", + "from google.colab import userdata\n", + "os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')\n", + "\n", + "class OpenAIModelFee(BaseModel):\n", + " model_name: str = Field(..., description=\"Name of the OpenAI model.\")\n", + " input_fee: str = Field(..., description=\"Fee for input token for the OpenAI model.\")\n", + " output_fee: str = Field(..., description=\"Fee for output token for the OpenAI model.\")\n", + "\n", + "async def extract_openai_fees():\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " result = await crawler.arun(\n", + " url='https://openai.com/api/pricing/',\n", + " word_count_threshold=1,\n", + " extraction_strategy=LLMExtractionStrategy(\n", + " provider=\"openai/gpt-4o\", api_token=os.getenv('OPENAI_API_KEY'),\n", + " schema=OpenAIModelFee.schema(),\n", + " extraction_type=\"schema\",\n", + " instruction=\"\"\"From the crawled content, extract all mentioned model names along with their fees for input and output tokens.\n", + " Do not miss any models in the entire content. One extracted model JSON format should look like this:\n", + " {\"model_name\": \"GPT-4\", \"input_fee\": \"US$10.00 / 1M tokens\", \"output_fee\": \"US$30.00 / 1M tokens\"}.\"\"\"\n", + " ),\n", + " bypass_cache=True,\n", + " )\n", + " print(len(result.extracted_content))\n", + "\n", + "# Uncomment the following line to run the OpenAI extraction example\n", + "await extract_openai_fees()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BypA5YxEyZQN" + }, + "source": [ + "### Advanced Multi-Page Crawling with JavaScript Execution" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Advanced Multi-Page Crawling with JavaScript Execution\n", + "\n", + "This example demonstrates Crawl4AI's ability to handle complex crawling scenarios, specifically extracting commits from multiple pages of a GitHub repository. The challenge here is that clicking the \"Next\" button doesn't load a new page, but instead uses asynchronous JavaScript to update the content. This is a common hurdle in modern web crawling.\n", + "\n", + "To overcome this, we use Crawl4AI's custom JavaScript execution to simulate clicking the \"Next\" button, and implement a custom hook to detect when new data has loaded. Our strategy involves comparing the first commit's text before and after \"clicking\" Next, waiting until it changes to confirm new data has rendered. This showcases Crawl4AI's flexibility in handling dynamic content and its ability to implement custom logic for even the most challenging crawling tasks." + ], + "metadata": { + "id": "tfkcVQ0b7mw-" + } + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "qUBKGpn3yZQN", + "outputId": "3e555b6a-ed33-42f4-cce9-499a923fbe17" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[LOG] 🌤️ Warming up the AsyncWebCrawler\n", + "[LOG] 🌞 AsyncWebCrawler is ready to crawl\n", + "[LOG] 🕸️ Crawling https://github.com/microsoft/TypeScript/commits/main using AsyncPlaywrightCrawlerStrategy...\n", + "[LOG] ✅ Crawled https://github.com/microsoft/TypeScript/commits/main successfully!\n", + "[LOG] 🚀 Crawling done for https://github.com/microsoft/TypeScript/commits/main, success: True, time taken: 5.16 seconds\n", + "[LOG] 🚀 Content extracted for https://github.com/microsoft/TypeScript/commits/main, success: True, time taken: 0.28 seconds\n", + "[LOG] 🔥 Extracting semantic blocks for https://github.com/microsoft/TypeScript/commits/main, Strategy: AsyncWebCrawler\n", + "[LOG] 🚀 Extraction done for https://github.com/microsoft/TypeScript/commits/main, time taken: 0.28 seconds.\n", + "Page 1: Found 35 commits\n", + "[LOG] 🕸️ Crawling https://github.com/microsoft/TypeScript/commits/main using AsyncPlaywrightCrawlerStrategy...\n", + "[LOG] ✅ Crawled https://github.com/microsoft/TypeScript/commits/main successfully!\n", + "[LOG] 🚀 Crawling done for https://github.com/microsoft/TypeScript/commits/main, success: True, time taken: 0.78 seconds\n", + "[LOG] 🚀 Content extracted for https://github.com/microsoft/TypeScript/commits/main, success: True, time taken: 0.90 seconds\n", + "[LOG] 🔥 Extracting semantic blocks for https://github.com/microsoft/TypeScript/commits/main, Strategy: AsyncWebCrawler\n", + "[LOG] 🚀 Extraction done for https://github.com/microsoft/TypeScript/commits/main, time taken: 0.90 seconds.\n", + "Page 2: Found 35 commits\n", + "[LOG] 🕸️ Crawling https://github.com/microsoft/TypeScript/commits/main using AsyncPlaywrightCrawlerStrategy...\n", + "[LOG] ✅ Crawled https://github.com/microsoft/TypeScript/commits/main successfully!\n", + "[LOG] 🚀 Crawling done for https://github.com/microsoft/TypeScript/commits/main, success: True, time taken: 2.00 seconds\n", + "[LOG] 🚀 Content extracted for https://github.com/microsoft/TypeScript/commits/main, success: True, time taken: 0.74 seconds\n", + "[LOG] 🔥 Extracting semantic blocks for https://github.com/microsoft/TypeScript/commits/main, Strategy: AsyncWebCrawler\n", + "[LOG] 🚀 Extraction done for https://github.com/microsoft/TypeScript/commits/main, time taken: 0.75 seconds.\n", + "Page 3: Found 35 commits\n", + "Successfully crawled 105 commits across 3 pages\n" + ] + } + ], + "source": [ + "import re\n", + "from bs4 import BeautifulSoup\n", + "\n", + "async def crawl_typescript_commits():\n", + " first_commit = \"\"\n", + " async def on_execution_started(page):\n", + " nonlocal first_commit\n", + " try:\n", + " while True:\n", + " await page.wait_for_selector('li.Box-sc-g0xbh4-0 h4')\n", + " commit = await page.query_selector('li.Box-sc-g0xbh4-0 h4')\n", + " commit = await commit.evaluate('(element) => element.textContent')\n", + " commit = re.sub(r'\\s+', '', commit)\n", + " if commit and commit != first_commit:\n", + " first_commit = commit\n", + " break\n", + " await asyncio.sleep(0.5)\n", + " except Exception as e:\n", + " print(f\"Warning: New content didn't appear after JavaScript execution: {e}\")\n", + "\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " crawler.crawler_strategy.set_hook('on_execution_started', on_execution_started)\n", + "\n", + " url = \"https://github.com/microsoft/TypeScript/commits/main\"\n", + " session_id = \"typescript_commits_session\"\n", + " all_commits = []\n", + "\n", + " js_next_page = \"\"\"\n", + " const button = document.querySelector('a[data-testid=\"pagination-next-button\"]');\n", + " if (button) button.click();\n", + " \"\"\"\n", + "\n", + " for page in range(3): # Crawl 3 pages\n", + " result = await crawler.arun(\n", + " url=url,\n", + " session_id=session_id,\n", + " css_selector=\"li.Box-sc-g0xbh4-0\",\n", + " js=js_next_page if page > 0 else None,\n", + " bypass_cache=True,\n", + " js_only=page > 0\n", + " )\n", + "\n", + " assert result.success, f\"Failed to crawl page {page + 1}\"\n", + "\n", + " soup = BeautifulSoup(result.cleaned_html, 'html.parser')\n", + " commits = soup.select(\"li\")\n", + " all_commits.extend(commits)\n", + "\n", + " print(f\"Page {page + 1}: Found {len(commits)} commits\")\n", + "\n", + " await crawler.crawler_strategy.kill_session(session_id)\n", + " print(f\"Successfully crawled {len(all_commits)} commits across 3 pages\")\n", + "\n", + "await crawl_typescript_commits()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EJRnYsp6yZQN" + }, + "source": [ + "### Using JsonCssExtractionStrategy for Fast Structured Output" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The JsonCssExtractionStrategy is a powerful feature of Crawl4AI that allows for precise, structured data extraction from web pages. Here's how it works:\n", + "\n", + "1. You define a schema that describes the pattern of data you're interested in extracting.\n", + "2. The schema includes a base selector that identifies repeating elements on the page.\n", + "3. Within the schema, you define fields, each with its own selector and type.\n", + "4. These field selectors are applied within the context of each base selector element.\n", + "5. The strategy supports nested structures, lists within lists, and various data types.\n", + "6. You can even include computed fields for more complex data manipulation.\n", + "\n", + "This approach allows for highly flexible and precise data extraction, transforming semi-structured web content into clean, structured JSON data. It's particularly useful for extracting consistent data patterns from pages like product listings, news articles, or search results.\n", + "\n", + "For more details and advanced usage, check out the full documentation on the Crawl4AI website." + ], + "metadata": { + "id": "1ZMqIzB_8SYp" + } + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "trCMR2T9yZQN", + "outputId": "718d36f4-cccf-40f4-8d8c-c3ba73524d16" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[LOG] 🌤️ Warming up the AsyncWebCrawler\n", + "[LOG] 🌞 AsyncWebCrawler is ready to crawl\n", + "[LOG] 🕸️ Crawling https://www.nbcnews.com/business using AsyncPlaywrightCrawlerStrategy...\n", + "[LOG] ✅ Crawled https://www.nbcnews.com/business successfully!\n", + "[LOG] 🚀 Crawling done for https://www.nbcnews.com/business, success: True, time taken: 7.00 seconds\n", + "[LOG] 🚀 Content extracted for https://www.nbcnews.com/business, success: True, time taken: 0.32 seconds\n", + "[LOG] 🔥 Extracting semantic blocks for https://www.nbcnews.com/business, Strategy: AsyncWebCrawler\n", + "[LOG] 🚀 Extraction done for https://www.nbcnews.com/business, time taken: 0.48 seconds.\n", + "Successfully extracted 11 news teasers\n", + "{\n", + " \"category\": \"Business News\",\n", + " \"headline\": \"NBC ripped up its Olympics playbook for 2024 \\u2014 so far, the new strategy paid off\",\n", + " \"summary\": \"The Olympics have long been key to NBCUniversal. Paris marked the 18th Olympic Games broadcast by NBC in the U.S.\",\n", + " \"time\": \"13h ago\",\n", + " \"image\": {\n", + " \"src\": \"https://media-cldnry.s-nbcnews.com/image/upload/t_focal-200x100,f_auto,q_auto:best/rockcms/2024-09/240903-nbc-olympics-ch-1344-c7a486.jpg\",\n", + " \"alt\": \"Mike Tirico.\"\n", + " },\n", + " \"link\": \"https://www.nbcnews.com/business\"\n", + "}\n" + ] + } + ], + "source": [ + "async def extract_news_teasers():\n", + " schema = {\n", + " \"name\": \"News Teaser Extractor\",\n", + " \"baseSelector\": \".wide-tease-item__wrapper\",\n", + " \"fields\": [\n", + " {\n", + " \"name\": \"category\",\n", + " \"selector\": \".unibrow span[data-testid='unibrow-text']\",\n", + " \"type\": \"text\",\n", + " },\n", + " {\n", + " \"name\": \"headline\",\n", + " \"selector\": \".wide-tease-item__headline\",\n", + " \"type\": \"text\",\n", + " },\n", + " {\n", + " \"name\": \"summary\",\n", + " \"selector\": \".wide-tease-item__description\",\n", + " \"type\": \"text\",\n", + " },\n", + " {\n", + " \"name\": \"time\",\n", + " \"selector\": \"[data-testid='wide-tease-date']\",\n", + " \"type\": \"text\",\n", + " },\n", + " {\n", + " \"name\": \"image\",\n", + " \"type\": \"nested\",\n", + " \"selector\": \"picture.teasePicture img\",\n", + " \"fields\": [\n", + " {\"name\": \"src\", \"type\": \"attribute\", \"attribute\": \"src\"},\n", + " {\"name\": \"alt\", \"type\": \"attribute\", \"attribute\": \"alt\"},\n", + " ],\n", + " },\n", + " {\n", + " \"name\": \"link\",\n", + " \"selector\": \"a[href]\",\n", + " \"type\": \"attribute\",\n", + " \"attribute\": \"href\",\n", + " },\n", + " ],\n", + " }\n", + "\n", + " extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True)\n", + "\n", + " async with AsyncWebCrawler(verbose=True) as crawler:\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " extraction_strategy=extraction_strategy,\n", + " bypass_cache=True,\n", + " )\n", + "\n", + " assert result.success, \"Failed to crawl the page\"\n", + "\n", + " news_teasers = json.loads(result.extracted_content)\n", + " print(f\"Successfully extracted {len(news_teasers)} news teasers\")\n", + " print(json.dumps(news_teasers[0], indent=2))\n", + "\n", + "await extract_news_teasers()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FnyVhJaByZQN" + }, + "source": [ + "## Speed Comparison\n", + "\n", + "Let's compare the speed of Crawl4AI with Firecrawl, a paid service. Note that we can't run Firecrawl in this Colab environment, so we'll simulate its performance based on previously recorded data." + ] + }, + { + "cell_type": "markdown", + "source": [ + "💡 **Note on Speed Comparison:**\n", + "\n", + "The speed test conducted here is running on Google Colab, where the internet speed and performance can vary and may not reflect optimal conditions. When we call Firecrawl's API, we're seeing its best performance, while Crawl4AI's performance is limited by Colab's network speed.\n", + "\n", + "For a more accurate comparison, it's recommended to run these tests on your own servers or computers with a stable and fast internet connection. Despite these limitations, Crawl4AI still demonstrates faster performance in this environment.\n", + "\n", + "If you run these tests locally, you may observe an even more significant speed advantage for Crawl4AI compared to other services." + ], + "metadata": { + "id": "agDD186f3wig" + } + }, + { + "cell_type": "code", + "source": [ + "!pip install firecrawl" + ], + "metadata": { + "id": "F7KwHv8G1LbY" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "91813zILyZQN", + "outputId": "663223db-ab89-4976-b233-05ceca62b19b" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Firecrawl (simulated):\n", + "Time taken: 4.38 seconds\n", + "Content length: 41967 characters\n", + "Images found: 49\n", + "\n", + "Crawl4AI (simple crawl):\n", + "Time taken: 4.22 seconds\n", + "Content length: 18221 characters\n", + "Images found: 49\n", + "\n", + "Crawl4AI (with JavaScript execution):\n", + "Time taken: 9.13 seconds\n", + "Content length: 34243 characters\n", + "Images found: 89\n" + ] + } + ], + "source": [ + "import os\n", + "from google.colab import userdata\n", + "os.environ['FIRECRAWL_API_KEY'] = userdata.get('FIRECRAWL_API_KEY')\n", + "import time\n", + "from firecrawl import FirecrawlApp\n", + "\n", + "async def speed_comparison():\n", + " # Simulated Firecrawl performance\n", + " app = FirecrawlApp(api_key=os.environ['FIRECRAWL_API_KEY'])\n", + " start = time.time()\n", + " scrape_status = app.scrape_url(\n", + " 'https://www.nbcnews.com/business',\n", + " params={'formats': ['markdown', 'html']}\n", + " )\n", + " end = time.time()\n", + " print(\"Firecrawl (simulated):\")\n", + " print(f\"Time taken: {end - start:.2f} seconds\")\n", + " print(f\"Content length: {len(scrape_status['markdown'])} characters\")\n", + " print(f\"Images found: {scrape_status['markdown'].count('cldnry.s-nbcnews.com')}\")\n", + " print()\n", + "\n", + " async with AsyncWebCrawler() as crawler:\n", + " # Crawl4AI simple crawl\n", + " start = time.time()\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " word_count_threshold=0,\n", + " bypass_cache=True,\n", + " verbose=False\n", + " )\n", + " end = time.time()\n", + " print(\"Crawl4AI (simple crawl):\")\n", + " print(f\"Time taken: {end - start:.2f} seconds\")\n", + " print(f\"Content length: {len(result.markdown)} characters\")\n", + " print(f\"Images found: {result.markdown.count('cldnry.s-nbcnews.com')}\")\n", + " print()\n", + "\n", + " # Crawl4AI with JavaScript execution\n", + " start = time.time()\n", + " result = await crawler.arun(\n", + " url=\"https://www.nbcnews.com/business\",\n", + " js_code=[\"const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();\"],\n", + " word_count_threshold=0,\n", + " bypass_cache=True,\n", + " verbose=False\n", + " )\n", + " end = time.time()\n", + " print(\"Crawl4AI (with JavaScript execution):\")\n", + " print(f\"Time taken: {end - start:.2f} seconds\")\n", + " print(f\"Content length: {len(result.markdown)} characters\")\n", + " print(f\"Images found: {result.markdown.count('cldnry.s-nbcnews.com')}\")\n", + "\n", + "await speed_comparison()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OBFFYVJIyZQN" + }, + "source": [ + "If you run on a local machine with a proper internet speed:\n", + "- Simple crawl: Crawl4AI is typically over 3-4 times faster than Firecrawl.\n", + "- With JavaScript execution: Even when executing JavaScript to load more content (potentially doubling the number of images found), Crawl4AI is still faster than Firecrawl's simple crawl.\n", + "\n", + "Please note that actual performance may vary depending on network conditions and the specific content being crawled." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A6_1RK1_yZQO" + }, + "source": [ + "## Conclusion\n", + "\n", + "In this notebook, we've explored the powerful features of Crawl4AI, including:\n", + "\n", + "1. Basic crawling\n", + "2. JavaScript execution and CSS selector usage\n", + "3. Proxy support\n", + "4. Structured data extraction with OpenAI\n", + "5. Advanced multi-page crawling with JavaScript execution\n", + "6. Fast structured output using JsonCssExtractionStrategy\n", + "7. Speed comparison with other services\n", + "\n", + "Crawl4AI offers a fast, flexible, and powerful solution for web crawling and data extraction tasks. Its asynchronous architecture and advanced features make it suitable for a wide range of applications, from simple web scraping to complex, multi-page data extraction scenarios.\n", + "\n", + "For more information and advanced usage, please visit the [Crawl4AI documentation](https://crawl4ai.com/mkdocs/).\n", + "\n", + "Happy crawling!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "colab": { + "provenance": [] + } }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Installation\n", - "\n", - "First, let's install Crawl4AI from GitHub:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install \"crawl4ai @ git+https://github.com/unclecode/crawl4ai.git\"\n", - "!pip install nest-asyncio\n", - "!playwright install" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, let's import the necessary libraries:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import asyncio\n", - "import nest_asyncio\n", - "from crawl4ai import AsyncWebCrawler\n", - "from crawl4ai.extraction_strategy import JsonCssExtractionStrategy, LLMExtractionStrategy\n", - "import json\n", - "import time\n", - "from pydantic import BaseModel, Field\n", - "\n", - "nest_asyncio.apply()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Basic Usage\n", - "\n", - "Let's start with a simple crawl example:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def simple_crawl():\n", - " async with AsyncWebCrawler(verbose=True) as crawler:\n", - " result = await crawler.arun(url=\"https://www.nbcnews.com/business\")\n", - " print(result.markdown[:500]) # Print first 500 characters\n", - "\n", - "await simple_crawl()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Advanced Features\n", - "\n", - "### Executing JavaScript and Using CSS Selectors" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def js_and_css():\n", - " async with AsyncWebCrawler(verbose=True) as crawler:\n", - " js_code = [\"const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();\"]\n", - " result = await crawler.arun(\n", - " url=\"https://www.nbcnews.com/business\",\n", - " js_code=js_code,\n", - " css_selector=\"article.tease-card\",\n", - " bypass_cache=True\n", - " )\n", - " print(result.extracted_content[:500]) # Print first 500 characters\n", - "\n", - "await js_and_css()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using a Proxy\n", - "\n", - "Note: You'll need to replace the proxy URL with a working proxy for this example to run successfully." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def use_proxy():\n", - " async with AsyncWebCrawler(verbose=True, proxy=\"http://your-proxy-url:port\") as crawler:\n", - " result = await crawler.arun(\n", - " url=\"https://www.nbcnews.com/business\",\n", - " bypass_cache=True\n", - " )\n", - " print(result.markdown[:500]) # Print first 500 characters\n", - "\n", - "# Uncomment the following line to run the proxy example\n", - "# await use_proxy()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Extracting Structured Data with OpenAI\n", - "\n", - "Note: You'll need to set your OpenAI API key as an environment variable for this example to work." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "class OpenAIModelFee(BaseModel):\n", - " model_name: str = Field(..., description=\"Name of the OpenAI model.\")\n", - " input_fee: str = Field(..., description=\"Fee for input token for the OpenAI model.\")\n", - " output_fee: str = Field(..., description=\"Fee for output token for the OpenAI model.\")\n", - "\n", - "async def extract_openai_fees():\n", - " async with AsyncWebCrawler(verbose=True) as crawler:\n", - " result = await crawler.arun(\n", - " url='https://openai.com/api/pricing/',\n", - " word_count_threshold=1,\n", - " extraction_strategy=LLMExtractionStrategy(\n", - " provider=\"openai/gpt-4o\", api_token=os.getenv('OPENAI_API_KEY'), \n", - " schema=OpenAIModelFee.schema(),\n", - " extraction_type=\"schema\",\n", - " instruction=\"\"\"From the crawled content, extract all mentioned model names along with their fees for input and output tokens. \n", - " Do not miss any models in the entire content. One extracted model JSON format should look like this: \n", - " {\"model_name\": \"GPT-4\", \"input_fee\": \"US$10.00 / 1M tokens\", \"output_fee\": \"US$30.00 / 1M tokens\"}.\"\"\"\n", - " ), \n", - " bypass_cache=True,\n", - " )\n", - " print(result.extracted_content)\n", - "\n", - "# Uncomment the following line to run the OpenAI extraction example\n", - "# await extract_openai_fees()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Advanced Multi-Page Crawling with JavaScript Execution" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import re\n", - "from bs4 import BeautifulSoup\n", - "\n", - "async def crawl_typescript_commits():\n", - " first_commit = \"\"\n", - " async def on_execution_started(page):\n", - " nonlocal first_commit \n", - " try:\n", - " while True:\n", - " await page.wait_for_selector('li.Box-sc-g0xbh4-0 h4')\n", - " commit = await page.query_selector('li.Box-sc-g0xbh4-0 h4')\n", - " commit = await commit.evaluate('(element) => element.textContent')\n", - " commit = re.sub(r'\\s+', '', commit)\n", - " if commit and commit != first_commit:\n", - " first_commit = commit\n", - " break\n", - " await asyncio.sleep(0.5)\n", - " except Exception as e:\n", - " print(f\"Warning: New content didn't appear after JavaScript execution: {e}\")\n", - "\n", - " async with AsyncWebCrawler(verbose=True) as crawler:\n", - " crawler.crawler_strategy.set_hook('on_execution_started', on_execution_started)\n", - "\n", - " url = \"https://github.com/microsoft/TypeScript/commits/main\"\n", - " session_id = \"typescript_commits_session\"\n", - " all_commits = []\n", - "\n", - " js_next_page = \"\"\"\n", - " const button = document.querySelector('a[data-testid=\"pagination-next-button\"]');\n", - " if (button) button.click();\n", - " \"\"\"\n", - "\n", - " for page in range(3): # Crawl 3 pages\n", - " result = await crawler.arun(\n", - " url=url,\n", - " session_id=session_id,\n", - " css_selector=\"li.Box-sc-g0xbh4-0\",\n", - " js=js_next_page if page > 0 else None,\n", - " bypass_cache=True,\n", - " js_only=page > 0\n", - " )\n", - "\n", - " assert result.success, f\"Failed to crawl page {page + 1}\"\n", - "\n", - " soup = BeautifulSoup(result.cleaned_html, 'html.parser')\n", - " commits = soup.select(\"li\")\n", - " all_commits.extend(commits)\n", - "\n", - " print(f\"Page {page + 1}: Found {len(commits)} commits\")\n", - "\n", - " await crawler.crawler_strategy.kill_session(session_id)\n", - " print(f\"Successfully crawled {len(all_commits)} commits across 3 pages\")\n", - "\n", - "await crawl_typescript_commits()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using JsonCssExtractionStrategy for Fast Structured Output" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def extract_news_teasers():\n", - " schema = {\n", - " \"name\": \"News Teaser Extractor\",\n", - " \"baseSelector\": \".wide-tease-item__wrapper\",\n", - " \"fields\": [\n", - " {\n", - " \"name\": \"category\",\n", - " \"selector\": \".unibrow span[data-testid='unibrow-text']\",\n", - " \"type\": \"text\",\n", - " },\n", - " {\n", - " \"name\": \"headline\",\n", - " \"selector\": \".wide-tease-item__headline\",\n", - " \"type\": \"text\",\n", - " },\n", - " {\n", - " \"name\": \"summary\",\n", - " \"selector\": \".wide-tease-item__description\",\n", - " \"type\": \"text\",\n", - " },\n", - " {\n", - " \"name\": \"time\",\n", - " \"selector\": \"[data-testid='wide-tease-date']\",\n", - " \"type\": \"text\",\n", - " },\n", - " {\n", - " \"name\": \"image\",\n", - " \"type\": \"nested\",\n", - " \"selector\": \"picture.teasePicture img\",\n", - " \"fields\": [\n", - " {\"name\": \"src\", \"type\": \"attribute\", \"attribute\": \"src\"},\n", - " {\"name\": \"alt\", \"type\": \"attribute\", \"attribute\": \"alt\"},\n", - " ],\n", - " },\n", - " {\n", - " \"name\": \"link\",\n", - " \"selector\": \"a[href]\",\n", - " \"type\": \"attribute\",\n", - " \"attribute\": \"href\",\n", - " },\n", - " ],\n", - " }\n", - "\n", - " extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True)\n", - "\n", - " async with AsyncWebCrawler(verbose=True) as crawler:\n", - " result = await crawler.arun(\n", - " url=\"https://www.nbcnews.com/business\",\n", - " extraction_strategy=extraction_strategy,\n", - " bypass_cache=True,\n", - " )\n", - "\n", - " assert result.success, \"Failed to crawl the page\"\n", - "\n", - " news_teasers = json.loads(result.extracted_content)\n", - " print(f\"Successfully extracted {len(news_teasers)} news teasers\")\n", - " print(json.dumps(news_teasers[0], indent=2))\n", - "\n", - "await extract_news_teasers()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Speed Comparison\n", - "\n", - "Let's compare the speed of Crawl4AI with Firecrawl, a paid service. Note that we can't run Firecrawl in this Colab environment, so we'll simulate its performance based on previously recorded data." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "\n", - "async def speed_comparison():\n", - " # Simulated Firecrawl performance\n", - " print(\"Firecrawl (simulated):\")\n", - " print(\"Time taken: 7.02 seconds\")\n", - " print(\"Content length: 42074 characters\")\n", - " print(\"Images found: 49\")\n", - " print()\n", - "\n", - " async with AsyncWebCrawler() as crawler:\n", - " # Crawl4AI simple crawl\n", - " start = time.time()\n", - " result = await crawler.arun(\n", - " url=\"https://www.nbcnews.com/business\",\n", - " word_count_threshold=0,\n", - " bypass_cache=True, \n", - " verbose=False\n", - " )\n", - " end = time.time()\n", - " print(\"Crawl4AI (simple crawl):\")\n", - " print(f\"Time taken: {end - start:.2f} seconds\")\n", - " print(f\"Content length: {len(result.markdown)} characters\")\n", - " print(f\"Images found: {result.markdown.count('cldnry.s-nbcnews.com')}\")\n", - " print()\n", - "\n", - " # Crawl4AI with JavaScript execution\n", - " start = time.time()\n", - " result = await crawler.arun(\n", - " url=\"https://www.nbcnews.com/business\",\n", - " js_code=[\"const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();\"],\n", - " word_count_threshold=0,\n", - " bypass_cache=True, \n", - " verbose=False\n", - " )\n", - " end = time.time()\n", - " print(\"Crawl4AI (with JavaScript execution):\")\n", - " print(f\"Time taken: {end - start:.2f} seconds\")\n", - " print(f\"Content length: {len(result.markdown)} characters\")\n", - " print(f\"Images found: {result.markdown.count('cldnry.s-nbcnews.com')}\")\n", - "\n", - "await speed_comparison()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As you can see, Crawl4AI outperforms Firecrawl significantly:\n", - "- Simple crawl: Crawl4AI is typically over 4 times faster than Firecrawl.\n", - "- With JavaScript execution: Even when executing JavaScript to load more content (potentially doubling the number of images found), Crawl4AI is still faster than Firecrawl's simple crawl.\n", - "\n", - "Please note that actual performance may vary depending on network conditions and the specific content being crawled." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "In this notebook, we've explored the powerful features of Crawl4AI, including:\n", - "\n", - "1. Basic crawling\n", - "2. JavaScript execution and CSS selector usage\n", - "3. Proxy support\n", - "4. Structured data extraction with OpenAI\n", - "5. Advanced multi-page crawling with JavaScript execution\n", - "6. Fast structured output using JsonCssExtractionStrategy\n", - "7. Speed comparison with other services\n", - "\n", - "Crawl4AI offers a fast, flexible, and powerful solution for web crawling and data extraction tasks. Its asynchronous architecture and advanced features make it suitable for a wide range of applications, from simple web scraping to complex, multi-page data extraction scenarios.\n", - "\n", - "For more information and advanced usage, please visit the [Crawl4AI documentation](https://crawl4ai.com/mkdocs/).\n", - "\n", - "Happy crawling!" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/examples/quickstart_async.py b/docs/examples/quickstart_async.py new file mode 100644 index 00000000..7dcc9183 --- /dev/null +++ b/docs/examples/quickstart_async.py @@ -0,0 +1,242 @@ +import asyncio +import time +import json +import os +import re +from bs4 import BeautifulSoup +from pydantic import BaseModel, Field +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy, LLMExtractionStrategy + +print("Crawl4AI: Advanced Web Crawling and Data Extraction") +print("GitHub Repository: https://github.com/unclecode/crawl4ai") +print("Twitter: @unclecode") +print("Website: https://crawl4ai.com") + +async def simple_crawl(): + print("\n--- Basic Usage ---") + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun(url="https://www.nbcnews.com/business") + print(result.markdown[:500]) # Print first 500 characters + +async def js_and_css(): + print("\n--- Executing JavaScript and Using CSS Selectors ---") + async with AsyncWebCrawler(verbose=True) as crawler: + js_code = ["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"] + result = await crawler.arun( + url="https://www.nbcnews.com/business", + js_code=js_code, + css_selector="article.tease-card", + bypass_cache=True + ) + print(result.extracted_content[:500]) # Print first 500 characters + +async def use_proxy(): + print("\n--- Using a Proxy ---") + print("Note: Replace 'http://your-proxy-url:port' with a working proxy to run this example.") + # Uncomment and modify the following lines to use a proxy + # async with AsyncWebCrawler(verbose=True, proxy="http://your-proxy-url:port") as crawler: + # result = await crawler.arun( + # url="https://www.nbcnews.com/business", + # bypass_cache=True + # ) + # print(result.markdown[:500]) # Print first 500 characters + +class OpenAIModelFee(BaseModel): + model_name: str = Field(..., description="Name of the OpenAI model.") + input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") + output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") + +async def extract_openai_fees(): + print("\n--- Extracting Structured Data with OpenAI ---") + print("Note: Set your OpenAI API key as an environment variable to run this example.") + if not os.getenv('OPENAI_API_KEY'): + print("OpenAI API key not found. Skipping this example.") + return + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url='https://openai.com/api/pricing/', + word_count_threshold=1, + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", api_token=os.getenv('OPENAI_API_KEY'), + schema=OpenAIModelFee.schema(), + extraction_type="schema", + instruction="""From the crawled content, extract all mentioned model names along with their fees for input and output tokens. + Do not miss any models in the entire content. One extracted model JSON format should look like this: + {"model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens"}.""" + ), + bypass_cache=True, + ) + print(result.extracted_content) + +async def crawl_typescript_commits(): + print("\n--- Advanced Multi-Page Crawling with JavaScript Execution ---") + first_commit = "" + async def on_execution_started(page): + nonlocal first_commit + try: + while True: + await page.wait_for_selector('li.Box-sc-g0xbh4-0 h4') + commit = await page.query_selector('li.Box-sc-g0xbh4-0 h4') + commit = await commit.evaluate('(element) => element.textContent') + commit = re.sub(r'\s+', '', commit) + if commit and commit != first_commit: + first_commit = commit + break + await asyncio.sleep(0.5) + except Exception as e: + print(f"Warning: New content didn't appear after JavaScript execution: {e}") + + async with AsyncWebCrawler(verbose=True) as crawler: + crawler.crawler_strategy.set_hook('on_execution_started', on_execution_started) + + url = "https://github.com/microsoft/TypeScript/commits/main" + session_id = "typescript_commits_session" + all_commits = [] + + js_next_page = """ + const button = document.querySelector('a[data-testid="pagination-next-button"]'); + if (button) button.click(); + """ + + for page in range(3): # Crawl 3 pages + result = await crawler.arun( + url=url, + session_id=session_id, + css_selector="li.Box-sc-g0xbh4-0", + js=js_next_page if page > 0 else None, + bypass_cache=True, + js_only=page > 0 + ) + + assert result.success, f"Failed to crawl page {page + 1}" + + soup = BeautifulSoup(result.cleaned_html, 'html.parser') + commits = soup.select("li") + all_commits.extend(commits) + + print(f"Page {page + 1}: Found {len(commits)} commits") + + await crawler.crawler_strategy.kill_session(session_id) + print(f"Successfully crawled {len(all_commits)} commits across 3 pages") + +async def extract_news_teasers(): + print("\n--- Using JsonCssExtractionStrategy for Fast Structured Output ---") + schema = { + "name": "News Teaser Extractor", + "baseSelector": ".wide-tease-item__wrapper", + "fields": [ + { + "name": "category", + "selector": ".unibrow span[data-testid='unibrow-text']", + "type": "text", + }, + { + "name": "headline", + "selector": ".wide-tease-item__headline", + "type": "text", + }, + { + "name": "summary", + "selector": ".wide-tease-item__description", + "type": "text", + }, + { + "name": "time", + "selector": "[data-testid='wide-tease-date']", + "type": "text", + }, + { + "name": "image", + "type": "nested", + "selector": "picture.teasePicture img", + "fields": [ + {"name": "src", "type": "attribute", "attribute": "src"}, + {"name": "alt", "type": "attribute", "attribute": "alt"}, + ], + }, + { + "name": "link", + "selector": "a[href]", + "type": "attribute", + "attribute": "href", + }, + ], + } + + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.nbcnews.com/business", + extraction_strategy=extraction_strategy, + bypass_cache=True, + ) + + assert result.success, "Failed to crawl the page" + + news_teasers = json.loads(result.extracted_content) + print(f"Successfully extracted {len(news_teasers)} news teasers") + print(json.dumps(news_teasers[0], indent=2)) + +async def speed_comparison(): + print("\n--- Speed Comparison ---") + print("Firecrawl (simulated):") + print("Time taken: 7.02 seconds") + print("Content length: 42074 characters") + print("Images found: 49") + print() + + async with AsyncWebCrawler() as crawler: + # Crawl4AI simple crawl + start = time.time() + result = await crawler.arun( + url="https://www.nbcnews.com/business", + word_count_threshold=0, + bypass_cache=True, + verbose=False + ) + end = time.time() + print("Crawl4AI (simple crawl):") + print(f"Time taken: {end - start:.2f} seconds") + print(f"Content length: {len(result.markdown)} characters") + print(f"Images found: {result.markdown.count('cldnry.s-nbcnews.com')}") + print() + + # Crawl4AI with JavaScript execution + start = time.time() + result = await crawler.arun( + url="https://www.nbcnews.com/business", + js_code=["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"], + word_count_threshold=0, + bypass_cache=True, + verbose=False + ) + end = time.time() + print("Crawl4AI (with JavaScript execution):") + print(f"Time taken: {end - start:.2f} seconds") + print(f"Content length: {len(result.markdown)} characters") + print(f"Images found: {result.markdown.count('cldnry.s-nbcnews.com')}") + + print("\nNote on Speed Comparison:") + print("The speed test conducted here may not reflect optimal conditions.") + print("When we call Firecrawl's API, we're seeing its best performance,") + print("while Crawl4AI's performance is limited by the local network speed.") + print("For a more accurate comparison, it's recommended to run these tests") + print("on servers with a stable and fast internet connection.") + print("Despite these limitations, Crawl4AI still demonstrates faster performance.") + print("If you run these tests in an environment with better network conditions,") + print("you may observe an even more significant speed advantage for Crawl4AI.") + +async def main(): + await simple_crawl() + await js_and_css() + await use_proxy() + await extract_openai_fees() + await crawl_typescript_commits() + await extract_news_teasers() + await speed_comparison() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/docs/examples/quickstart.py b/docs/examples/quickstart_sync.py similarity index 100% rename from docs/examples/quickstart.py rename to docs/examples/quickstart_sync.py From 396f430022164698f8f240da5efe6bf2e1623077 Mon Sep 17 00:00:00 2001 From: unclecode Date: Thu, 12 Sep 2024 15:49:49 +0800 Subject: [PATCH 13/57] Refactor AsyncCrawlerStrategy to return AsyncCrawlResponse This commit refactors the AsyncCrawlerStrategy class in the async_crawler_strategy.py file to modify the return types of the crawl and crawl_many methods. Instead of returning strings, these methods now return instances of the AsyncCrawlResponse class from the pydantic module. The AsyncCrawlResponse class contains the crawled HTML, response headers, and status code. This change improves the clarity and consistency of the code. --- crawl4ai/async_crawler_strategy.py | 42 +++++++++++++++++++++++++----- crawl4ai/async_webcrawler.py | 8 ++++-- crawl4ai/models.py | 4 ++- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index 3840260e..7b24620c 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -12,6 +12,7 @@ import json, uuid import hashlib from pathlib import Path from playwright.async_api import ProxySettings +from pydantic import BaseModel def calculate_semaphore_count(): cpu_count = os.cpu_count() @@ -20,13 +21,18 @@ def calculate_semaphore_count(): memory_based_cap = int(memory_gb / 2) # Assume 2GB per instance return min(base_count, memory_based_cap) +class AsyncCrawlResponse(BaseModel): + html: str + response_headers: Dict[str, str] + status_code: int + class AsyncCrawlerStrategy(ABC): @abstractmethod - async def crawl(self, url: str, **kwargs) -> str: + async def crawl(self, url: str, **kwargs) -> AsyncCrawlResponse: pass @abstractmethod - async def crawl_many(self, urls: List[str], **kwargs) -> List[str]: + async def crawl_many(self, urls: List[str], **kwargs) -> List[AsyncCrawlResponse]: pass @abstractmethod @@ -140,7 +146,10 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): for sid in expired_sessions: asyncio.create_task(self.kill_session(sid)) - async def crawl(self, url: str, **kwargs) -> str: + async def crawl(self, url: str, **kwargs) -> AsyncCrawlResponse: + response_headers = {} + status_code = None + self._cleanup_expired_sessions() session_id = kwargs.get("session_id") if session_id: @@ -168,13 +177,25 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): if self.use_cached_html: cache_file_path = os.path.join(Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest()) if os.path.exists(cache_file_path): + html = "" with open(cache_file_path, "r") as f: - return f.read() + html = f.read() + # retrieve response headers and status code from cache + with open(cache_file_path + ".meta", "r") as f: + meta = json.load(f) + response_headers = meta.get("response_headers", {}) + status_code = meta.get("status_code") + response = AsyncCrawlResponse(html=html, response_headers=response_headers, status_code=status_code) + return response if not kwargs.get("js_only", False): await self.execute_hook('before_goto', page) - await page.goto(url, wait_until="domcontentloaded", timeout=60000) + response = await page.goto(url, wait_until="domcontentloaded", timeout=60000) await self.execute_hook('after_goto', page) + + # Get status code and headers + status_code = response.status + response_headers = response.headers await page.wait_for_selector('body') await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") @@ -202,8 +223,15 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): cache_file_path = os.path.join(Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest()) with open(cache_file_path, "w", encoding="utf-8") as f: f.write(html) + # store response headers and status code in cache + with open(cache_file_path + ".meta", "w", encoding="utf-8") as f: + json.dump({ + "response_headers": response_headers, + "status_code": status_code + }, f) - return html + response = AsyncCrawlResponse(html=html, response_headers=response_headers, status_code=status_code) + return response except Error as e: raise Error(f"Failed to crawl {url}: {str(e)}") finally: @@ -218,7 +246,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): # except Exception as e: # raise Exception(f"Failed to crawl {url}: {str(e)}") - async def crawl_many(self, urls: List[str], **kwargs) -> List[str]: + async def crawl_many(self, urls: List[str], **kwargs) -> List[AsyncCrawlResponse]: semaphore_count = kwargs.get('semaphore_count', calculate_semaphore_count()) semaphore = asyncio.Semaphore(semaphore_count) diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index 3cdc9ac1..ceca09f5 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -8,7 +8,7 @@ from .models import CrawlResult from .async_database import async_db_manager from .chunking_strategy import * from .extraction_strategy import * -from .async_crawler_strategy import AsyncCrawlerStrategy, AsyncPlaywrightCrawlerStrategy +from .async_crawler_strategy import AsyncCrawlerStrategy, AsyncPlaywrightCrawlerStrategy, AsyncCrawlResponse from .content_scrapping_strategy import WebScrappingStrategy from .config import MIN_WORD_THRESHOLD, IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD from .utils import ( @@ -101,7 +101,8 @@ class AsyncWebCrawler: t1 = time.time() if user_agent: self.crawler_strategy.update_user_agent(user_agent) - html = await self.crawler_strategy.crawl(url, **kwargs) + async_response : AsyncCrawlResponse = await self.crawler_strategy.crawl(url, **kwargs) + html = sanitize_input_encode(async_response.html) t2 = time.time() if verbose: print( @@ -121,8 +122,11 @@ class AsyncWebCrawler: screenshot_data, verbose, bool(cached), + async_response=async_response, **kwargs, ) + crawl_result.status_code = async_response.status_code + crawl_result.responser_headers = async_response.response_headers crawl_result.success = bool(html) crawl_result.session_id = kwargs.get("session_id", None) return crawl_result diff --git a/crawl4ai/models.py b/crawl4ai/models.py index e48441b8..eefb0cb9 100644 --- a/crawl4ai/models.py +++ b/crawl4ai/models.py @@ -17,4 +17,6 @@ class CrawlResult(BaseModel): extracted_content: Optional[str] = None metadata: Optional[dict] = None error_message: Optional[str] = None - session_id: Optional[str] = None \ No newline at end of file + session_id: Optional[str] = None + responser_headers: Optional[dict] = None + status_code: Optional[int] = None \ No newline at end of file From 30807f5535dd8ed5eeabced9ce0555ba86d9c9a4 Mon Sep 17 00:00:00 2001 From: unclecode Date: Thu, 12 Sep 2024 16:11:20 +0800 Subject: [PATCH 14/57] Remove excluded tags from website content --- crawl4ai/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crawl4ai/utils.py b/crawl4ai/utils.py index 61d1e333..b5736ad8 100644 --- a/crawl4ai/utils.py +++ b/crawl4ai/utils.py @@ -441,6 +441,10 @@ def get_content_of_website_optimized(url: str, html: str, word_count_threshold: image_description_min_word_threshold = kwargs.get('image_description_min_word_threshold', IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD) + for tag in kwargs.get('excluded_tags', []) or []: + for el in body.select(tag): + el.decompose() + if css_selector: selected_elements = body.select(css_selector) if not selected_elements: From b179aa9b6fb810379576aba8eab97c58db6a948a Mon Sep 17 00:00:00 2001 From: unclecode Date: Thu, 12 Sep 2024 16:50:52 +0800 Subject: [PATCH 15/57] Refactor website content and setup.py descriptions for consistent terminology --- pages/index_pooling.html | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/index_pooling.html b/pages/index_pooling.html index 920801d1..02128f84 100644 --- a/pages/index_pooling.html +++ b/pages/index_pooling.html @@ -50,7 +50,7 @@
-

🔥🕷️ Crawl4AI: Open-source LLM Friendly Web Scrapper

+

🔥🕷️ Crawl4AI: Open-source LLM Friendly Web scraper

diff --git a/setup.py b/setup.py index 841f85a8..321756f4 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ transformer_requirements = [req for req in requirements if req.startswith(("tran setup( name="Crawl4AI", version="0.2.77", - description="🔥🕷️ Crawl4AI: Open-source LLM Friendly Web Crawler & Scrapper", + description="🔥🕷️ Crawl4AI: Open-source LLM Friendly Web Crawler & scraper", long_description=open("README.md", encoding="utf-8").read(), long_description_content_type="text/markdown", url="https://github.com/unclecode/crawl4ai", From d628bc403443bc389f59efb1ef97779faeebb4c9 Mon Sep 17 00:00:00 2001 From: unclecode Date: Thu, 12 Sep 2024 17:35:45 +0800 Subject: [PATCH 16/57] Refactor content_scrapping_strategy.py to remove excluded tags --- crawl4ai/content_scrapping_strategy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 56868354..126dbaed 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -43,6 +43,10 @@ class WebScrappingStrategy(ContentScrappingStrategy): image_description_min_word_threshold = kwargs.get('image_description_min_word_threshold', IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD) + for tag in kwargs.get('excluded_tags', []) or []: + for el in body.select(tag): + el.decompose() + if css_selector: selected_elements = body.select(css_selector) if not selected_elements: From 4d48bd31ca99d34cb9464e7c1ce774f567809db6 Mon Sep 17 00:00:00 2001 From: unclecode Date: Tue, 24 Sep 2024 20:52:08 +0800 Subject: [PATCH 17/57] Push async version last changes for merge to main branch --- CONTRIBUTORS.md | 5 +- README.md | 72 +- crawl4ai/__init__.py | 2 +- crawl4ai/async_crawler_strategy.py | 107 +- crawl4ai/async_webcrawler.py | 9 +- crawl4ai/content_scrapping_strategy.py | 11 +- crawl4ai/crawler_strategy.py | 12 + crawl4ai/model_loader.py | 2 - crawl4ai/web_crawler.py | 2 +- docs/.DS_Store | Bin 6148 -> 6148 bytes docs/examples/quickstart_async.py | 317 +++-- docs/examples/research_assistant.py | 2 +- docs/examples/sample_ecommerce.html | 106 ++ .../research_assistant_audio_not_completed.py | 2 +- .../api/core_classes_and_functions.md | 141 ++ .../api/detailed_api_documentation.md | 338 +++++ docs/md _sync/assets/DankMono-Bold.woff2 | Bin 0 -> 33480 bytes docs/md _sync/assets/DankMono-Italic.woff2 | Bin 0 -> 32468 bytes docs/md _sync/assets/DankMono-Regular.woff2 | Bin 0 -> 30528 bytes docs/md _sync/assets/Monaco.woff | Bin 0 -> 20096 bytes docs/md _sync/assets/dmvendor.css | 127 ++ docs/md _sync/assets/highlight.css | 0 docs/md _sync/assets/highlight.min.js | 1213 +++++++++++++++++ docs/md _sync/assets/highlight_init.js | 6 + docs/md _sync/assets/styles.css | 153 +++ docs/md _sync/changelog.md | 102 ++ docs/md _sync/contact.md | 25 + docs/md _sync/demo.md | 231 ++++ docs/md _sync/examples/hooks_auth.md | 100 ++ docs/md _sync/examples/index.md | 29 + .../examples/js_execution_css_filtering.md | 44 + docs/md _sync/examples/llm_extraction.md | 90 ++ docs/md _sync/examples/research_assistant.md | 248 ++++ docs/md _sync/examples/summarization.md | 108 ++ .../full_details/advanced_features.md | 138 ++ .../full_details/chunking_strategies.md | 133 ++ .../full_details/crawl_request_parameters.md | 130 ++ .../full_details/crawl_result_class.md | 120 ++ .../full_details/extraction_strategies.md | 116 ++ docs/md _sync/index.md | 101 ++ docs/md _sync/installation.md | 193 +++ docs/md _sync/interactive_content.html | 28 + docs/md _sync/introduction.md | 29 + docs/md _sync/quickstart.md | 204 +++ docs/md/examples/hooks_auth.md | 144 +- docs/md/examples/index.md | 4 + .../md/examples/js_execution_css_filtering.md | 120 +- docs/md/examples/json_css_extraction.md | 142 ++ docs/md/examples/llm_extraction.md | 183 ++- docs/md/examples/research_assistant.md | 174 +-- docs/md/examples/summarization.md | 169 ++- .../advanced_jsoncss_extraction.md | 282 ++++ .../full_details/crawl_request_parameters.md | 99 +- docs/md/full_details/crawl_result_class.md | 42 +- docs/md/full_details/extraction_strategies.md | 245 ++-- docs/md/index.md | 55 +- docs/md/installation.md | 205 +-- docs/md/quickstart.md | 325 +++-- mkdocs.yml | 6 +- requirements.txt | 88 +- setup.py | 31 +- 61 files changed, 6219 insertions(+), 891 deletions(-) create mode 100644 docs/examples/sample_ecommerce.html create mode 100644 docs/md _sync/api/core_classes_and_functions.md create mode 100644 docs/md _sync/api/detailed_api_documentation.md create mode 100644 docs/md _sync/assets/DankMono-Bold.woff2 create mode 100644 docs/md _sync/assets/DankMono-Italic.woff2 create mode 100644 docs/md _sync/assets/DankMono-Regular.woff2 create mode 100644 docs/md _sync/assets/Monaco.woff create mode 100644 docs/md _sync/assets/dmvendor.css create mode 100644 docs/md _sync/assets/highlight.css create mode 100644 docs/md _sync/assets/highlight.min.js create mode 100644 docs/md _sync/assets/highlight_init.js create mode 100644 docs/md _sync/assets/styles.css create mode 100644 docs/md _sync/changelog.md create mode 100644 docs/md _sync/contact.md create mode 100644 docs/md _sync/demo.md create mode 100644 docs/md _sync/examples/hooks_auth.md create mode 100644 docs/md _sync/examples/index.md create mode 100644 docs/md _sync/examples/js_execution_css_filtering.md create mode 100644 docs/md _sync/examples/llm_extraction.md create mode 100644 docs/md _sync/examples/research_assistant.md create mode 100644 docs/md _sync/examples/summarization.md create mode 100644 docs/md _sync/full_details/advanced_features.md create mode 100644 docs/md _sync/full_details/chunking_strategies.md create mode 100644 docs/md _sync/full_details/crawl_request_parameters.md create mode 100644 docs/md _sync/full_details/crawl_result_class.md create mode 100644 docs/md _sync/full_details/extraction_strategies.md create mode 100644 docs/md _sync/index.md create mode 100644 docs/md _sync/installation.md create mode 100644 docs/md _sync/interactive_content.html create mode 100644 docs/md _sync/introduction.md create mode 100644 docs/md _sync/quickstart.md create mode 100644 docs/md/examples/json_css_extraction.md create mode 100644 docs/md/full_details/advanced_jsoncss_extraction.md diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0e45ca85..0b5dcede 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,12 +6,14 @@ We would like to thank the following people for their contributions to Crawl4AI: - [Unclecode](https://github.com/unclecode) - Project Creator and Main Developer - [Nasrin](https://github.com/ntohidi) - Project Manager and Developer +- [Aravind Karnam](https://github.com/aravindkarnam) - Developer ## Community Contributors -- [Aravind Karnam](https://github.com/aravindkarnam) - Developed textual description extraction feature - [FractalMind](https://github.com/FractalMind) - Created the first official Docker Hub image and fixed Dockerfile errors - [ketonkss4](https://github.com/ketonkss4) - Identified Selenium's new capabilities, helping reduce dependencies +- [jonymusky](https://github.com/jonymusky) - Javascript execution documentation, and wait_for +- [datehoer](https://github.com/datehoer) - Add browser prxy support ## Other Contributors @@ -19,7 +21,6 @@ We would like to thank the following people for their contributions to Crawl4AI: - [Shiv Kumar](https://github.com/shivkumar0757) - [QIN2DIM](https://github.com/QIN2DIM) - ## Acknowledgements We also want to thank all the users who have reported bugs, suggested features, or helped in any other way to make Crawl4AI better. diff --git a/README.md b/README.md index a1c2c449..138e9abc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Crawl4AI Async Version 🕷️🤖 +# Crawl4AI 0.3.0 Async Version 🕷️🤖 [![GitHub Stars](https://img.shields.io/github/stars/unclecode/crawl4ai?style=social)](https://github.com/unclecode/crawl4ai/stargazers) [![GitHub Forks](https://img.shields.io/github/forks/unclecode/crawl4ai?style=social)](https://github.com/unclecode/crawl4ai/network/members) @@ -43,18 +43,78 @@ Crawl4AI simplifies asynchronous web crawling and data extraction, making it acc ## Installation 🛠️ +Crawl4AI offers flexible installation options to suit various use cases. You can install it as a Python package or use Docker. + ### Using pip 🐍 + +Choose the installation option that best fits your needs: + +#### Basic Installation + +For basic web crawling and scraping tasks: + ```bash -virtualenv venv -source venv/bin/activate -pip install "crawl4ai @ git+https://github.com/unclecode/crawl4ai.git" +pip install crawl4ai +``` + +#### Installation with PyTorch + +For advanced text clustering (includes CosineSimilarity cluster strategy): + +```bash +pip install crawl4ai[torch] +``` + +#### Installation with Transformers + +For text summarization and Hugging Face models: + +```bash +pip install crawl4ai[transformer] +``` + +#### Installation with Synchronous Version + +If you need the synchronous version using Selenium: + +```bash +pip install crawl4ai[sync] +``` + +#### Installation with Cosine Similarity + +For using the cosine similarity strategy: + +```bash +pip install crawl4ai[cosine] +``` + +#### Full Installation + +For all features: + +```bash +pip install crawl4ai[all] +``` + +After installation, run the following command to install Playwright dependencies: + +```bash +playwright install +``` + +If you've installed the "torch", "transformer", or "all" options, it's recommended to run: + +```bash +crawl4ai-download-models ``` ### Using Docker 🐳 ```bash # For Mac users (M1/M2) -# docker build --platform linux/amd64 -t crawl4ai . +docker build --platform linux/amd64 -t crawl4ai . +# For other users docker build -t crawl4ai . docker run -d -p 8000:80 crawl4ai ``` @@ -66,6 +126,8 @@ docker pull unclecode/crawl4ai:latest docker run -d -p 8000:80 unclecode/crawl4ai:latest ``` +For more detailed installation instructions and options, please refer to our [Installation Guide](https://crawl4ai.com/mkdocs/installation). + ## Quick Start 🚀 ```python diff --git a/crawl4ai/__init__.py b/crawl4ai/__init__.py index 9c5ac3e0..7d758fda 100644 --- a/crawl4ai/__init__.py +++ b/crawl4ai/__init__.py @@ -2,7 +2,7 @@ from .web_crawler import WebCrawler from .async_webcrawler import AsyncWebCrawler from .models import CrawlResult -__version__ = "0.2.77" +__version__ = "0.3.0" __all__ = [ "WebCrawler", diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index 7b24620c..d32f25c9 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -52,6 +52,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): self.use_cached_html = use_cached_html self.user_agent = kwargs.get("user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") self.proxy = kwargs.get("proxy") + self.headless = kwargs.get("headless", True) self.headers = {} self.sessions = {} self.session_ttl = 1800 @@ -80,7 +81,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): self.playwright = await async_playwright().start() if self.browser is None: browser_args = { - "headless": True, + "headless": self.headless, # "headless": False, "args": [ "--disable-gpu", @@ -145,6 +146,31 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): if current_time - last_used > self.session_ttl] for sid in expired_sessions: asyncio.create_task(self.kill_session(sid)) + + + async def csp_compliant_wait(self, page: Page, user_wait_function: str, timeout: float = 30000): + wrapper_js = f""" + async () => {{ + const userFunction = {user_wait_function}; + const startTime = Date.now(); + while (true) {{ + if (await userFunction()) {{ + return true; + }} + if (Date.now() - startTime > {timeout}) {{ + throw new Error('Timeout waiting for condition'); + }} + await new Promise(resolve => setTimeout(resolve, 100)); + }} + }} + """ + + try: + await page.evaluate(wrapper_js) + except TimeoutError: + raise TimeoutError(f"Timeout after {timeout}ms waiting for condition") + except Exception as e: + raise RuntimeError(f"Error in wait condition: {str(e)}") async def crawl(self, url: str, **kwargs) -> AsyncCrawlResponse: response_headers = {} @@ -196,6 +222,9 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): # Get status code and headers status_code = response.status response_headers = response.headers + else: + status_code = 200 + response_headers = {} await page.wait_for_selector('body') await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") @@ -203,7 +232,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): js_code = kwargs.get("js_code", kwargs.get("js", self.js_code)) if js_code: if isinstance(js_code, str): - await page.evaluate(js_code) + r = await page.evaluate(js_code) elif isinstance(js_code, list): for js in js_code: await page.evaluate(js) @@ -212,6 +241,37 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): await page.wait_for_load_state('networkidle') # Check for on execution even await self.execute_hook('on_execution_started', page) + + # New code to handle the wait_for parameter + # Example usage: + # await crawler.crawl( + # url, + # js_code="// some JavaScript code", + # wait_for="""() => { + # return document.querySelector('#my-element') !== null; + # }""" + # ) + # Example of using a CSS selector: + # await crawler.crawl( + # url, + # wait_for="#my-element" + # ) + wait_for = kwargs.get("wait_for") + if wait_for: + try: + await self.csp_compliant_wait(page, wait_for, timeout=kwargs.get("timeout", 30000)) + except Exception as e: + raise RuntimeError(f"Custom wait condition failed: {str(e)}") + # try: + # await page.wait_for_function(wait_for) + # # if callable(wait_for): + # # await page.wait_for_function(wait_for) + # # elif isinstance(wait_for, str): + # # await page.wait_for_selector(wait_for) + # # else: + # # raise ValueError("wait_for must be either a callable or a CSS selector string") + # except Error as e: + # raise Error(f"Custom wait condition failed: {str(e)}") html = await page.content() page = await self.execute_hook('before_return_html', page, html) @@ -246,6 +306,49 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): # except Exception as e: # raise Exception(f"Failed to crawl {url}: {str(e)}") + async def execute_js(self, session_id: str, js_code: str, wait_for_js: str = None, wait_for_css: str = None) -> AsyncCrawlResponse: + """ + Execute JavaScript code in a specific session and optionally wait for a condition. + + :param session_id: The ID of the session to execute the JS code in. + :param js_code: The JavaScript code to execute. + :param wait_for_js: JavaScript condition to wait for after execution. + :param wait_for_css: CSS selector to wait for after execution. + :return: AsyncCrawlResponse containing the page's HTML and other information. + :raises ValueError: If the session does not exist. + """ + if not session_id: + raise ValueError("Session ID must be provided") + + if session_id not in self.sessions: + raise ValueError(f"No active session found for session ID: {session_id}") + + context, page, last_used = self.sessions[session_id] + + try: + await page.evaluate(js_code) + + if wait_for_js: + await page.wait_for_function(wait_for_js) + + if wait_for_css: + await page.wait_for_selector(wait_for_css) + + # Get the updated HTML content + html = await page.content() + + # Get response headers and status code (assuming these are available) + response_headers = await page.evaluate("() => JSON.stringify(performance.getEntriesByType('resource')[0].responseHeaders)") + status_code = await page.evaluate("() => performance.getEntriesByType('resource')[0].responseStatus") + + # Update the last used time for this session + self.sessions[session_id] = (context, page, time.time()) + + return AsyncCrawlResponse(html=html, response_headers=response_headers, status_code=status_code) + except Error as e: + raise Error(f"Failed to execute JavaScript or wait for condition in session {session_id}: {str(e)}") + + async def crawl_many(self, urls: List[str], **kwargs) -> List[AsyncCrawlResponse]: semaphore_count = kwargs.get('semaphore_count', calculate_semaphore_count()) semaphore = asyncio.Semaphore(semaphore_count) diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index ceca09f5..34b192e5 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -80,6 +80,7 @@ class AsyncWebCrawler: word_count_threshold = max(word_count_threshold, MIN_WORD_THRESHOLD) + async_response : AsyncCrawlResponse = None cached = None screenshot_data = None extracted_content = None @@ -125,8 +126,8 @@ class AsyncWebCrawler: async_response=async_response, **kwargs, ) - crawl_result.status_code = async_response.status_code - crawl_result.responser_headers = async_response.response_headers + crawl_result.status_code = async_response.status_code if async_response else 200 + crawl_result.responser_headers = async_response.response_headers if async_response else {} crawl_result.success = bool(html) crawl_result.session_id = kwargs.get("session_id", None) return crawl_result @@ -224,11 +225,11 @@ class AsyncWebCrawler: if isinstance(extraction_strategy, JsonCssExtractionStrategy) or isinstance(extraction_strategy, JsonCssExtractionStrategy): extraction_strategy.verbose = verbose extracted_content = extraction_strategy.run(url, [html]) - extracted_content = json.dumps(extracted_content, indent=4, default=str) + extracted_content = json.dumps(extracted_content, indent=4, default=str, ensure_ascii=False) else: sections = chunking_strategy.chunk(markdown) extracted_content = extraction_strategy.run(url, sections) - extracted_content = json.dumps(extracted_content, indent=4, default=str) + extracted_content = json.dumps(extracted_content, indent=4, default=str, ensure_ascii=False) if verbose: print( diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 126dbaed..e3d2c57f 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -50,7 +50,16 @@ class WebScrappingStrategy(ContentScrappingStrategy): if css_selector: selected_elements = body.select(css_selector) if not selected_elements: - raise InvalidCSSSelectorError(f"Invalid CSS selector, No elements found for CSS selector: {css_selector}") + return { + 'markdown': '', + 'cleaned_html': '', + 'success': True, + 'media': {'images': [], 'videos': [], 'audios': []}, + 'links': {'internal': [], 'external': []}, + 'metadata': {}, + 'message': f"No elements found for CSS selector: {css_selector}" + } + # raise InvalidCSSSelectorError(f"Invalid CSS selector, No elements found for CSS selector: {css_selector}") body = soup.new_tag('div') for el in selected_elements: body.append(el) diff --git a/crawl4ai/crawler_strategy.py b/crawl4ai/crawler_strategy.py index 4d049069..5d6864b5 100644 --- a/crawl4ai/crawler_strategy.py +++ b/crawl4ai/crawler_strategy.py @@ -258,6 +258,18 @@ class LocalSeleniumCrawlerStrategy(CrawlerStrategy): lambda driver: driver.execute_script("return document.readyState") == "complete" ) + # Optionally, wait for some condition after executing the JS code : Contributed by (https://github.com/jonymusky) + wait_for = kwargs.get('wait_for', False) + if wait_for: + if callable(wait_for): + print("[LOG] 🔄 Waiting for condition...") + WebDriverWait(self.driver, 20).until(wait_for) + else: + print("[LOG] 🔄 Waiting for condition...") + WebDriverWait(self.driver, 20).until( + EC.presence_of_element_located((By.CSS_SELECTOR, wait_for)) + ) + if not can_not_be_done_headless: html = sanitize_input_encode(self.driver.page_source) self.driver = self.execute_hook('before_return_html', self.driver, html) diff --git a/crawl4ai/model_loader.py b/crawl4ai/model_loader.py index 7608ca51..e33e53f4 100644 --- a/crawl4ai/model_loader.py +++ b/crawl4ai/model_loader.py @@ -80,7 +80,6 @@ def load_bge_small_en_v1_5(): model, device = set_model_device(model) return tokenizer, model - @lru_cache() def load_text_classifier(): from transformers import AutoTokenizer, AutoModelForSequenceClassification @@ -147,7 +146,6 @@ def load_nltk_punkt(): nltk.download('punkt') return nltk.data.find('tokenizers/punkt') - @lru_cache() def load_spacy_model(): import spacy diff --git a/crawl4ai/web_crawler.py b/crawl4ai/web_crawler.py index 3eda6b45..7dea56ca 100644 --- a/crawl4ai/web_crawler.py +++ b/crawl4ai/web_crawler.py @@ -201,7 +201,7 @@ class WebCrawler: sections = chunking_strategy.chunk(markdown) extracted_content = extraction_strategy.run(url, sections) - extracted_content = json.dumps(extracted_content, indent=4, default=str) + extracted_content = json.dumps(extracted_content, indent=4, default=str, ensure_ascii=False) if verbose: print(f"[LOG] 🚀 Extraction done for {url}, time taken: {time.time() - t:.2f} seconds.") diff --git a/docs/.DS_Store b/docs/.DS_Store index 94e8e06d0b3c0eea52a799b60b44125cd5fb1383..2502f79ff38d95e01298ce3cf6e393092fec9dda 100644 GIT binary patch delta 310 zcmZoMXfc=|#>B)qu~2NHo}wrd0|Nsi1A_nqLjgk$Ln=dYQh9N~#*NDvCmV>c)N=qu zDi{(Oa*<_|a`Kaax^^ZNBn1{L;LXVxZn|pivMR z4o(ivcmc`kYC{t<104kmQCJzu~2NHo}wrt0|NsP3otMgFyt_#G8C5u7v)VXRA*$|{FF(Fb+ZC< nB-3Vg4t@@xmd%39- { + return Array.from(document.querySelectorAll('article.tease-card')).length > 10; + }""" + + # wait_for can be also just a css selector + # wait_for = "article.tease-card:nth-child(10)" + async with AsyncWebCrawler(verbose=True) as crawler: - js_code = ["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"] + js_code = [ + "const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();" + ] result = await crawler.arun( url="https://www.nbcnews.com/business", - js_code=js_code, + # js_code=js_code, css_selector="article.tease-card", - bypass_cache=True + # wait_for=wait_for, + bypass_cache=True, ) print(result.extracted_content[:500]) # Print first 500 characters async def use_proxy(): print("\n--- Using a Proxy ---") - print("Note: Replace 'http://your-proxy-url:port' with a working proxy to run this example.") + print( + "Note: Replace 'http://your-proxy-url:port' with a working proxy to run this example." + ) # Uncomment and modify the following lines to use a proxy # async with AsyncWebCrawler(verbose=True, proxy="http://your-proxy-url:port") as crawler: # result = await crawler.arun( @@ -45,42 +69,88 @@ async def use_proxy(): class OpenAIModelFee(BaseModel): model_name: str = Field(..., description="Name of the OpenAI model.") input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") - output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") + output_fee: str = Field( + ..., description="Fee for output token for the OpenAI model." + ) -async def extract_openai_fees(): +async def extract_structured_data_using_llm(): print("\n--- Extracting Structured Data with OpenAI ---") - print("Note: Set your OpenAI API key as an environment variable to run this example.") - if not os.getenv('OPENAI_API_KEY'): + print( + "Note: Set your OpenAI API key as an environment variable to run this example." + ) + if not os.getenv("OPENAI_API_KEY"): print("OpenAI API key not found. Skipping this example.") return - + async with AsyncWebCrawler(verbose=True) as crawler: result = await crawler.arun( - url='https://openai.com/api/pricing/', + url="https://openai.com/api/pricing/", word_count_threshold=1, extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", api_token=os.getenv('OPENAI_API_KEY'), + provider="openai/gpt-4o", + api_token=os.getenv("OPENAI_API_KEY"), schema=OpenAIModelFee.schema(), extraction_type="schema", instruction="""From the crawled content, extract all mentioned model names along with their fees for input and output tokens. Do not miss any models in the entire content. One extracted model JSON format should look like this: - {"model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens"}.""" - ), + {"model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens"}.""", + ), bypass_cache=True, ) print(result.extracted_content) -async def crawl_typescript_commits(): +async def extract_structured_data_using_css_extractor(): + print("\n--- Using JsonCssExtractionStrategy for Fast Structured Output ---") + schema = { + "name": "Coinbase Crypto Prices", + "baseSelector": ".cds-tableRow-t45thuk", + "fields": [ + { + "name": "crypto", + "selector": "td:nth-child(1) h2", + "type": "text", + }, + { + "name": "symbol", + "selector": "td:nth-child(1) p", + "type": "text", + }, + { + "name": "price", + "selector": "td:nth-child(2)", + "type": "text", + } + ], + } + + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.coinbase.com/explore", + extraction_strategy=extraction_strategy, + bypass_cache=True, + ) + + assert result.success, "Failed to crawl the page" + + news_teasers = json.loads(result.extracted_content) + print(f"Successfully extracted {len(news_teasers)} news teasers") + print(json.dumps(news_teasers[0], indent=2)) + +# Advanced Session-Based Crawling with Dynamic Content 🔄 +async def crawl_dynamic_content_pages_method_1(): print("\n--- Advanced Multi-Page Crawling with JavaScript Execution ---") first_commit = "" + async def on_execution_started(page): - nonlocal first_commit + nonlocal first_commit try: while True: - await page.wait_for_selector('li.Box-sc-g0xbh4-0 h4') - commit = await page.query_selector('li.Box-sc-g0xbh4-0 h4') - commit = await commit.evaluate('(element) => element.textContent') - commit = re.sub(r'\s+', '', commit) + await page.wait_for_selector("li.Box-sc-g0xbh4-0 h4") + commit = await page.query_selector("li.Box-sc-g0xbh4-0 h4") + commit = await commit.evaluate("(element) => element.textContent") + commit = re.sub(r"\s+", "", commit) if commit and commit != first_commit: first_commit = commit break @@ -89,7 +159,7 @@ async def crawl_typescript_commits(): print(f"Warning: New content didn't appear after JavaScript execution: {e}") async with AsyncWebCrawler(verbose=True) as crawler: - crawler.crawler_strategy.set_hook('on_execution_started', on_execution_started) + crawler.crawler_strategy.set_hook("on_execution_started", on_execution_started) url = "https://github.com/microsoft/TypeScript/commits/main" session_id = "typescript_commits_session" @@ -107,12 +177,13 @@ async def crawl_typescript_commits(): css_selector="li.Box-sc-g0xbh4-0", js=js_next_page if page > 0 else None, bypass_cache=True, - js_only=page > 0 + js_only=page > 0, + headless=False, ) assert result.success, f"Failed to crawl page {page + 1}" - soup = BeautifulSoup(result.cleaned_html, 'html.parser') + soup = BeautifulSoup(result.cleaned_html, "html.parser") commits = soup.select("li") all_commits.extend(commits) @@ -121,64 +192,133 @@ async def crawl_typescript_commits(): await crawler.crawler_strategy.kill_session(session_id) print(f"Successfully crawled {len(all_commits)} commits across 3 pages") -async def extract_news_teasers(): - print("\n--- Using JsonCssExtractionStrategy for Fast Structured Output ---") - schema = { - "name": "News Teaser Extractor", - "baseSelector": ".wide-tease-item__wrapper", - "fields": [ - { - "name": "category", - "selector": ".unibrow span[data-testid='unibrow-text']", - "type": "text", - }, - { - "name": "headline", - "selector": ".wide-tease-item__headline", - "type": "text", - }, - { - "name": "summary", - "selector": ".wide-tease-item__description", - "type": "text", - }, - { - "name": "time", - "selector": "[data-testid='wide-tease-date']", - "type": "text", - }, - { - "name": "image", - "type": "nested", - "selector": "picture.teasePicture img", - "fields": [ - {"name": "src", "type": "attribute", "attribute": "src"}, - {"name": "alt", "type": "attribute", "attribute": "alt"}, - ], - }, - { - "name": "link", - "selector": "a[href]", - "type": "attribute", - "attribute": "href", - }, - ], - } - - extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) +async def crawl_dynamic_content_pages_method_2(): + print("\n--- Advanced Multi-Page Crawling with JavaScript Execution ---") async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://www.nbcnews.com/business", - extraction_strategy=extraction_strategy, - bypass_cache=True, - ) + url = "https://github.com/microsoft/TypeScript/commits/main" + session_id = "typescript_commits_session" + all_commits = [] + last_commit = "" - assert result.success, "Failed to crawl the page" + js_next_page_and_wait = """ + (async () => { + const getCurrentCommit = () => { + const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4'); + return commits.length > 0 ? commits[0].textContent.trim() : null; + }; - news_teasers = json.loads(result.extracted_content) - print(f"Successfully extracted {len(news_teasers)} news teasers") - print(json.dumps(news_teasers[0], indent=2)) + const initialCommit = getCurrentCommit(); + const button = document.querySelector('a[data-testid="pagination-next-button"]'); + if (button) button.click(); + + // Poll for changes + while (true) { + await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms + const newCommit = getCurrentCommit(); + if (newCommit && newCommit !== initialCommit) { + break; + } + } + })(); + """ + + schema = { + "name": "Commit Extractor", + "baseSelector": "li.Box-sc-g0xbh4-0", + "fields": [ + { + "name": "title", + "selector": "h4.markdown-title", + "type": "text", + "transform": "strip", + }, + ], + } + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + for page in range(3): # Crawl 3 pages + result = await crawler.arun( + url=url, + session_id=session_id, + css_selector="li.Box-sc-g0xbh4-0", + extraction_strategy=extraction_strategy, + js_code=js_next_page_and_wait if page > 0 else None, + js_only=page > 0, + bypass_cache=True, + headless=False, + ) + + assert result.success, f"Failed to crawl page {page + 1}" + + commits = json.loads(result.extracted_content) + all_commits.extend(commits) + + print(f"Page {page + 1}: Found {len(commits)} commits") + + await crawler.crawler_strategy.kill_session(session_id) + print(f"Successfully crawled {len(all_commits)} commits across 3 pages") + +async def crawl_dynamic_content_pages_method_3(): + print("\n--- Advanced Multi-Page Crawling with JavaScript Execution using `wait_for` ---") + + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://github.com/microsoft/TypeScript/commits/main" + session_id = "typescript_commits_session" + all_commits = [] + + js_next_page = """ + const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4'); + if (commits.length > 0) { + window.lastCommit = commits[0].textContent.trim(); + } + const button = document.querySelector('a[data-testid="pagination-next-button"]'); + if (button) button.click(); + """ + + wait_for = """() => { + const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4'); + if (commits.length === 0) return false; + const firstCommit = commits[0].textContent.trim(); + return firstCommit !== window.lastCommit; + }""" + + schema = { + "name": "Commit Extractor", + "baseSelector": "li.Box-sc-g0xbh4-0", + "fields": [ + { + "name": "title", + "selector": "h4.markdown-title", + "type": "text", + "transform": "strip", + }, + ], + } + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + for page in range(3): # Crawl 3 pages + result = await crawler.arun( + url=url, + session_id=session_id, + css_selector="li.Box-sc-g0xbh4-0", + extraction_strategy=extraction_strategy, + js_code=js_next_page if page > 0 else None, + wait_for=wait_for if page > 0 else None, + js_only=page > 0, + bypass_cache=True, + headless=False, + ) + + assert result.success, f"Failed to crawl page {page + 1}" + + commits = json.loads(result.extracted_content) + all_commits.extend(commits) + + print(f"Page {page + 1}: Found {len(commits)} commits") + + await crawler.crawler_strategy.kill_session(session_id) + print(f"Successfully crawled {len(all_commits)} commits across 3 pages") async def speed_comparison(): print("\n--- Speed Comparison ---") @@ -194,8 +334,8 @@ async def speed_comparison(): result = await crawler.arun( url="https://www.nbcnews.com/business", word_count_threshold=0, - bypass_cache=True, - verbose=False + bypass_cache=True, + verbose=False, ) end = time.time() print("Crawl4AI (simple crawl):") @@ -208,10 +348,12 @@ async def speed_comparison(): start = time.time() result = await crawler.arun( url="https://www.nbcnews.com/business", - js_code=["const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();"], + js_code=[ + "const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); loadMoreButton && loadMoreButton.click();" + ], word_count_threshold=0, - bypass_cache=True, - verbose=False + bypass_cache=True, + verbose=False, ) end = time.time() print("Crawl4AI (with JavaScript execution):") @@ -233,10 +375,13 @@ async def main(): await simple_crawl() await js_and_css() await use_proxy() - await extract_openai_fees() - await crawl_typescript_commits() - await extract_news_teasers() + await extract_structured_data_using_llm() + await extract_structured_data_using_css_extractor() + await crawl_dynamic_content_pages_method_1() + await crawl_dynamic_content_pages_method_2() + await crawl_dynamic_content_pages_method_3() await speed_comparison() + if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/docs/examples/research_assistant.py b/docs/examples/research_assistant.py index f5dc9983..de35ce84 100644 --- a/docs/examples/research_assistant.py +++ b/docs/examples/research_assistant.py @@ -1,4 +1,4 @@ -# Make sur to install the required packageschainlit and groq +# Make sure to install the required packageschainlit and groq import os, time from openai import AsyncOpenAI import chainlit as cl diff --git a/docs/examples/sample_ecommerce.html b/docs/examples/sample_ecommerce.html new file mode 100644 index 00000000..4698d9c6 --- /dev/null +++ b/docs/examples/sample_ecommerce.html @@ -0,0 +1,106 @@ + + + + + + Sample E-commerce Page for JsonCssExtractionStrategy Testing + + + +

Sample E-commerce Product Catalog

+
+ + + + \ No newline at end of file diff --git a/docs/examples/tmp/research_assistant_audio_not_completed.py b/docs/examples/tmp/research_assistant_audio_not_completed.py index 59fb90e5..e0ad2b4f 100644 --- a/docs/examples/tmp/research_assistant_audio_not_completed.py +++ b/docs/examples/tmp/research_assistant_audio_not_completed.py @@ -1,4 +1,4 @@ -# Make sur to install the required packageschainlit and groq +# Make sure to install the required packageschainlit and groq import os, time from openai import AsyncOpenAI import chainlit as cl diff --git a/docs/md _sync/api/core_classes_and_functions.md b/docs/md _sync/api/core_classes_and_functions.md new file mode 100644 index 00000000..198fd427 --- /dev/null +++ b/docs/md _sync/api/core_classes_and_functions.md @@ -0,0 +1,141 @@ +# Core Classes and Functions + +## Overview + +In this section, we will delve into the core classes and functions that make up the Crawl4AI library. This includes the `WebCrawler` class, various `CrawlerStrategy` classes, `ChunkingStrategy` classes, and `ExtractionStrategy` classes. Understanding these core components will help you leverage the full power of Crawl4AI for your web crawling and data extraction needs. + +## WebCrawler Class + +The `WebCrawler` class is the main class you'll interact with. It provides the interface for crawling web pages and extracting data. + +### Initialization + +```python +from crawl4ai import WebCrawler + +# Create an instance of WebCrawler +crawler = WebCrawler() +``` + +### Methods + +- **`warmup()`**: Prepares the crawler for use, such as loading necessary models. +- **`run(url: str, **kwargs)`**: Runs the crawler on the specified URL with optional parameters for customization. + +```python +crawler.warmup() +result = crawler.run(url="https://www.nbcnews.com/business") +print(result) +``` + +## CrawlerStrategy Classes + +The `CrawlerStrategy` classes define how the web crawling is executed. The base class is `CrawlerStrategy`, which is extended by specific implementations like `LocalSeleniumCrawlerStrategy`. + +### CrawlerStrategy Base Class + +An abstract base class that defines the interface for different crawler strategies. + +```python +from abc import ABC, abstractmethod + +class CrawlerStrategy(ABC): + @abstractmethod + def crawl(self, url: str, **kwargs) -> str: + pass + + @abstractmethod + def take_screenshot(self, save_path: str): + pass + + @abstractmethod + def update_user_agent(self, user_agent: str): + pass + + @abstractmethod + def set_hook(self, hook_type: str, hook: Callable): + pass +``` + +### LocalSeleniumCrawlerStrategy Class + +A concrete implementation of `CrawlerStrategy` that uses Selenium to crawl web pages. + +#### Initialization + +```python +from crawl4ai.crawler_strategy import LocalSeleniumCrawlerStrategy + +strategy = LocalSeleniumCrawlerStrategy(js_code=["console.log('Hello, world!');"]) +``` + +#### Methods + +- **`crawl(url: str, **kwargs)`**: Crawls the specified URL. +- **`take_screenshot(save_path: str)`**: Takes a screenshot of the current page. +- **`update_user_agent(user_agent: str)`**: Updates the user agent for the browser. +- **`set_hook(hook_type: str, hook: Callable)`**: Sets a hook for various events. + +```python +result = strategy.crawl("https://www.example.com") +strategy.take_screenshot("screenshot.png") +strategy.update_user_agent("Mozilla/5.0") +strategy.set_hook("before_get_url", lambda: print("About to get URL")) +``` + +## ChunkingStrategy Classes + +The `ChunkingStrategy` classes define how the text from a web page is divided into chunks. Here are a few examples: + +### RegexChunking Class + +Splits text using regular expressions. + +```python +from crawl4ai.chunking_strategy import RegexChunking + +chunker = RegexChunking(patterns=[r'\n\n']) +chunks = chunker.chunk("This is a sample text. It will be split into chunks.") +``` + +### NlpSentenceChunking Class + +Uses NLP to split text into sentences. + +```python +from crawl4ai.chunking_strategy import NlpSentenceChunking + +chunker = NlpSentenceChunking() +chunks = chunker.chunk("This is a sample text. It will be split into sentences.") +``` + +## ExtractionStrategy Classes + +The `ExtractionStrategy` classes define how meaningful content is extracted from the chunks. Here are a few examples: + +### CosineStrategy Class + +Clusters text chunks based on cosine similarity. + +```python +from crawl4ai.extraction_strategy import CosineStrategy + +extractor = CosineStrategy(semantic_filter="finance", word_count_threshold=10) +extracted_content = extractor.extract(url="https://www.example.com", html="...") +``` + +### LLMExtractionStrategy Class + +Uses a Language Model to extract meaningful blocks from HTML. + +```python +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +extractor = LLMExtractionStrategy(provider='openai', api_token='your_api_token', instruction='Extract only news about AI.') +extracted_content = extractor.extract(url="https://www.example.com", html="...") +``` + +## Conclusion + +By understanding these core classes and functions, you can customize and extend Crawl4AI to suit your specific web crawling and data extraction needs. Happy crawling! 🕷️🤖 + diff --git a/docs/md _sync/api/detailed_api_documentation.md b/docs/md _sync/api/detailed_api_documentation.md new file mode 100644 index 00000000..aaf0e34f --- /dev/null +++ b/docs/md _sync/api/detailed_api_documentation.md @@ -0,0 +1,338 @@ +# Detailed API Documentation + +## Overview + +This section provides comprehensive documentation for the Crawl4AI API, covering all classes, methods, and their parameters. This guide will help you understand how to utilize the API to its full potential, enabling efficient web crawling and data extraction. + +## WebCrawler Class + +The `WebCrawler` class is the primary interface for crawling web pages and extracting data. + +### Initialization + +```python +from crawl4ai import WebCrawler + +crawler = WebCrawler() +``` + +### Methods + +#### `warmup()` + +Prepares the crawler for use, such as loading necessary models. + +```python +crawler.warmup() +``` + +#### `run(url: str, **kwargs) -> CrawlResult` + +Crawls the specified URL and returns the result. + +- **Parameters:** + - `url` (str): The URL to crawl. + - `**kwargs`: Additional parameters for customization. + +- **Returns:** + - `CrawlResult`: An object containing the crawl result. + +- **Example:** + +```python +result = crawler.run(url="https://www.nbcnews.com/business") +print(result) +``` + +### CrawlResult Class + +Represents the result of a crawl operation. + +- **Attributes:** + - `url` (str): The URL of the crawled page. + - `html` (str): The raw HTML of the page. + - `success` (bool): Whether the crawl was successful. + - `cleaned_html` (Optional[str]): The cleaned HTML. + - `media` (Dict[str, List[Dict]]): Media tags in the page (images, audio, video). + - `links` (Dict[str, List[Dict]]): Links in the page (external, internal). + - `screenshot` (Optional[str]): Base64 encoded screenshot. + - `markdown` (Optional[str]): Extracted content in Markdown format. + - `extracted_content` (Optional[str]): Extracted meaningful content. + - `metadata` (Optional[dict]): Metadata from the page. + - `error_message` (Optional[str]): Error message if any. + +## CrawlerStrategy Classes + +The `CrawlerStrategy` classes define how the web crawling is executed. + +### CrawlerStrategy Base Class + +An abstract base class for different crawler strategies. + +#### Methods + +- **`crawl(url: str, **kwargs) -> str`**: Crawls the specified URL. +- **`take_screenshot(save_path: str)`**: Takes a screenshot of the current page. +- **`update_user_agent(user_agent: str)`**: Updates the user agent for the browser. +- **`set_hook(hook_type: str, hook: Callable)`**: Sets a hook for various events. + +### LocalSeleniumCrawlerStrategy Class + +Uses Selenium to crawl web pages. + +#### Initialization + +```python +from crawl4ai.crawler_strategy import LocalSeleniumCrawlerStrategy + +strategy = LocalSeleniumCrawlerStrategy(js_code=["console.log('Hello, world!');"]) +``` + +#### Methods + +- **`crawl(url: str, **kwargs)`**: Crawls the specified URL. +- **`take_screenshot(save_path: str)`**: Takes a screenshot of the current page. +- **`update_user_agent(user_agent: str)`**: Updates the user agent for the browser. +- **`set_hook(hook_type: str, hook: Callable)`**: Sets a hook for various events. + +#### Example + +```python +result = strategy.crawl("https://www.example.com") +strategy.take_screenshot("screenshot.png") +strategy.update_user_agent("Mozilla/5.0") +strategy.set_hook("before_get_url", lambda: print("About to get URL")) +``` + +## ChunkingStrategy Classes + +The `ChunkingStrategy` classes define how the text from a web page is divided into chunks. + +### RegexChunking Class + +Splits text using regular expressions. + +#### Initialization + +```python +from crawl4ai.chunking_strategy import RegexChunking + +chunker = RegexChunking(patterns=[r'\n\n']) +``` + +#### Methods + +- **`chunk(text: str) -> List[str]`**: Splits the text into chunks. + +#### Example + +```python +chunks = chunker.chunk("This is a sample text. It will be split into chunks.") +``` + +### NlpSentenceChunking Class + +Uses NLP to split text into sentences. + +#### Initialization + +```python +from crawl4ai.chunking_strategy import NlpSentenceChunking + +chunker = NlpSentenceChunking() +``` + +#### Methods + +- **`chunk(text: str) -> List[str]`**: Splits the text into sentences. + +#### Example + +```python +chunks = chunker.chunk("This is a sample text. It will be split into sentences.") +``` + +### TopicSegmentationChunking Class + +Uses the TextTiling algorithm to segment text into topics. + +#### Initialization + +```python +from crawl4ai.chunking_strategy import TopicSegmentationChunking + +chunker = TopicSegmentationChunking(num_keywords=3) +``` + +#### Methods + +- **`chunk(text: str) -> List[str]`**: Splits the text into topic-based segments. + +#### Example + +```python +chunks = chunker.chunk("This is a sample text. It will be split into topic-based segments.") +``` + +### FixedLengthWordChunking Class + +Splits text into chunks of fixed length based on the number of words. + +#### Initialization + +```python +from crawl4ai.chunking_strategy import FixedLengthWordChunking + +chunker = FixedLengthWordChunking(chunk_size=100) +``` + +#### Methods + +- **`chunk(text: str) -> List[str]`**: Splits the text into fixed-length word chunks. + +#### Example + +```python +chunks = chunker.chunk("This is a sample text. It will be split into fixed-length word chunks.") +``` + +### SlidingWindowChunking Class + +Uses a sliding window approach to chunk text. + +#### Initialization + +```python +from crawl4ai.chunking_strategy import SlidingWindowChunking + +chunker = SlidingWindowChunking(window_size=100, step=50) +``` + +#### Methods + +- **`chunk(text: str) -> List[str]`**: Splits the text using a sliding window approach. + +#### Example + +```python +chunks = chunker.chunk("This is a sample text. It will be split using a sliding window approach.") +``` + +## ExtractionStrategy Classes + +The `ExtractionStrategy` classes define how meaningful content is extracted from the chunks. + +### NoExtractionStrategy Class + +Returns the entire HTML content without any modification. + +#### Initialization + +```python +from crawl4ai.extraction_strategy import NoExtractionStrategy + +extractor = NoExtractionStrategy() +``` + +#### Methods + +- **`extract(url: str, html: str) -> str`**: Returns the HTML content. + +#### Example + +```python +extracted_content = extractor.extract(url="https://www.example.com", html="...") +``` + +### LLMExtractionStrategy Class + +Uses a Language Model to extract meaningful blocks from HTML. + +#### Initialization + +```python +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +extractor = LLMExtractionStrategy(provider='openai', api_token='your_api_token', instruction='Extract only news about AI.') +``` + +#### Methods + +- **`extract(url: str, html: str) -> str`**: Extracts meaningful content using the LLM. + +#### Example + +```python +extracted_content = extractor.extract(url="https://www.example.com", html="...") +``` + +### CosineStrategy Class + +Clusters text chunks based on cosine similarity. + +#### Initialization + +```python +from crawl4ai.extraction_strategy import CosineStrategy + +extractor = CosineStrategy(semantic_filter="finance", word_count_threshold=10) +``` + +#### Methods + +- **`extract(url: str, html: str) -> str`**: Extracts clusters of text based on cosine similarity. + +#### Example + +```python +extracted_content = extractor.extract(url="https://www.example.com", html="...") +``` + +### TopicExtractionStrategy Class + +Uses the TextTiling algorithm to segment HTML content into topics and extract keywords. + +#### Initialization + +```python +from crawl4ai.extraction_strategy import TopicExtractionStrategy + +extractor = TopicExtractionStrategy(num_keywords=3) +``` + +#### Methods + +- **`extract(url: str, html: str) -> str`**: Extracts topic-based segments and keywords. + +#### Example + +```python +extracted_content = extractor.extract(url="https://www.example.com", html="...") +``` + +## Parameters + +Here are the common parameters used across various classes and methods: + +- **`url`** (str): The URL to crawl. +- **`html`** (str): The HTML content of the page. +- **`user_agent`** (str): The user agent for the HTTP requests. +- **`patterns`** (list): A list of regular expression patterns for chunking. +- **`num_keywords`** (int): Number of keywords for topic extraction. +- **`chunk_size`** (int): Number of words in each chunk. +- **`window_size`** (int): Number of words in the sliding window. +- **`step`** (int): Step size for the sliding window. +- **`semantic_filter`** (str): Keywords for filtering relevant documents. +- **`word_count_threshold`** (int): Minimum number of words per cluster. +- **`max_dist`** (float): Maximum cophenetic distance for clustering. +- **`linkage_method`** (str): Linkage method for hierarchical clustering. +- **`top_k`** (int): Number of top categories to extract. +- **`provider`** ( + +str): Provider for language model completions. +- **`api_token`** (str): API token for the provider. +- **`instruction`** (str): Instruction to guide the LLM extraction. + +## Conclusion + +This detailed API documentation provides a thorough understanding of the classes, methods, and parameters in the Crawl4AI library. With this knowledge, you can effectively use the API to perform advanced web crawling and data extraction tasks. \ No newline at end of file diff --git a/docs/md _sync/assets/DankMono-Bold.woff2 b/docs/md _sync/assets/DankMono-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3072fd8567c7f38769e8fa161b92417f2630f902 GIT binary patch literal 33480 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 MK{-$E|6vLM0LSWEPXGV_ literal 0 HcmV?d00001 diff --git a/docs/md _sync/assets/DankMono-Italic.woff2 b/docs/md _sync/assets/DankMono-Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1d01ea6d73be14be9ac9014865475ee73c597fb5 GIT binary patch literal 32468 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 j0=7(^{K1R0G5BwXVt^ZBLDyZBYynI literal 0 HcmV?d00001 diff --git a/docs/md _sync/assets/DankMono-Regular.woff2 b/docs/md _sync/assets/DankMono-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..99c1425ce4f3f324c29c0d4ac465bf73ffa363e9 GIT binary patch literal 30528 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

31>i z1}HVj-9GVQZ<*GWn_AUH1G;v(dpHd&^NbhZ!dtry4&u{$TvYINum;gjPIa zOt8fi=CVH+s@N@dpFP0>=lsupU50lVEjftn;Kv|Q8^Fm|zUMA%EjVU>c*K^I`ZN)0 z-X}9o&7+2LgFu=6!DfK|+!0%t9f(J9mdee3p)+sBr5UyQupK9w9ff*LXKtkX+*yta zeSx|=do~p)i1}zi)QPl+J;iK66`<9O^yLU<(!*@`z(D6MMeOu#J2xH{)V9oEa2V}a z7P9r&ChPAg@1EN2Bg027e((>Zy?joae(Skj|Y&GkSV;M4gv0TOMAZ$e{8 zv6j=sd+f;ZG8OU7V##~;S|2@!r(_b@V-coXm6Jq5smw;j;f2QC@%mNZuq)=%LoCSj z2h=iVeU_E-UNI8r{A-{AD7+3(&VR$mQT;OI?a3YL2w~)F^(7`nMw*C&(dA`E+v*FV&wL zJzZ2(z*{~o_2)X>^?H+}UN89m$lmC^wD=eH^X19sKxbVm%a=DG7V+vD&L%*_nCp$j zSqFaj*w-d`1Mh&Dv&^_!neScEd%?pK%0^V*5OBv0mje@{b-FqkhyJRKGLU*5nX2P> zD-%&^R?*5G|7{EmLPvGVf-BYSxAwFcUK%Hj$W5 z;zt+Kti5I=(3wBXF*F^TI*}D$T*V86vael(eBnJ*6F>u z#9`hi;lasIc!+)+k%Hx-9zhZ`Fiuz_L z9Ya&T6zgugSxZC>ne=#1G+g`U)LF=cw&||>WBSc|Gk>^@Xvd{gOoR7iWn&bf_3(VE z`yM>I7BH_f>PU0}M;38}O(l22R|8sZEz-sVwzSxW@RK;B5B+f6o3VF?AJQ^h&)*<{rbn_t-Ybs^lIk^m< zG~QTRB0DfkEl8NNXJ=g*QKjaOxk~6Fc-SDGp}JEi?o86*yAc|C;A^35>26OZ5xC|R zH?$%rs!y3;0@nNvNLhLKImNEfor7Wu>ubi&%#hfLjQH#VUg>?2Szl*SSs`OPYt^sA z(lcx>q}@*EftTt}oeq)5Fg`+)wazmpd9wqe8Lb-|Oac9rGfz0%K{aB3?b#TUdj8>r zE$U%FuTkoY5c(ta6i{G3yZi6kbFYx1QwmEI?FJ)!%rMqdK8w#r=+kCVL~Vs3gIvNx zbBBWhHwoSR+$Dv^12i)`96KmrBS$dv^_xBgXlRs+o*ABc09<5P%=XYP|$y%o6zJ zlQ3^kQZL_zw_%E);PQs`K|jEGePPXl{$SOq<@+2~#F7H2^-w50GDY^x7vpI#P*gyp3{@OtF+&v>gNsombA{(-X4cd(si!EuQEc3A z6onf^OEXBBxj{Ap0|gbssUN8}AU`&~_P3;OboOt*R%S^5?z2V8dqCFBySNXD*lb3- zoRZAjT{`#dl`VDA7`%HPyB*`6`6p=C*_~(`mg6YU(AmV0B5y%2{COaO4Ft;n1|~(O zTY5xF?O5X>s!<c%yV~6uwUsIP;or(kq`q`Jrem^r*&pVn&FM(!-}H9WHl=9irR(VIU6+@o~X6m zR^$z@!lRo;;lO`+2Arl+hw(Bf*=XN?Cq>rL1}eb0*96rMVvU9}gTr|cD|<*TXPtjn z8=1WnC^E5JMo|-NQV|iUnE_Ea`z5!iWD+ZD!EZnxQH^BrqE!X{;?xWwlv_-~ zt`}}z-at_wBbgG1ffr9aUV*j%O-#iRN(}x8`7=)@&vIAfxw$_JJml?!2QY4D~sc%2tvcAD5OL5#vdH{+~~^TSkSAadmX8^Qe-0 ze{oxx(>yVQKe=$GblQm9JO~|W7eSdUkn0w8C}nuxq~J1xxx3#6wSUFluIo9dTx})$ zyZUUnt`4CVBfPux1rY$pL#BAjo>m#Hwqp{c4mPnSK%-R6JvtLT$);2+2I<8NGo_eq zK0L*idMd4|s6LBeD!T|;xn7uEb~)Oe+bE{Id0rfF|4DrXABeCX>HmKz0G}VbiEsN3 z3nmvYIz&xK-kY1b*;}w>TeY|L!9Lr%O%cKjOPX$&mhHGgYPw-s zw&QxjkxILsZ>*3?8|%DLtwGG=07RHj#vvF%F`OVN03gDIq8XOs0YsQk#tHuvq4k)$ zK9|};Ou4F?w(CoXDOYvVmaY#mb4pWGV>4C{AwgaaEhk2VoQ^X_hB>kZIZW@eUsk#>=`5<20|< zo9%AD4C6H4=VSQUF1ur6oaSZS_Lcc<4BF_l?FaL#`+3J?etw+S{V=~9&s>}N-GA{a!-@9!79towRJS=CM3^`&y9TB|pjt)eKax@omL-Cn;atNQLAzmRYG!|~R_h6QU` z*P`L^jVxKla#y^{)sHvHl+({T_W*+pGtwC2O)}LCv(2;2Wv^gmtJ4TBaS=etP3u?=B7KI_G{05(JNm4JAd@=N832> z?S^rhmv!4mY3Uo2O{Fi!KbT5qU9!5|Ml&fd&1IpF2 zxd-KRZz}+Zh2aEA(F_Y9h7%-3Gc1Rpy!R<6r#oh%=+LD@r!L)k^y<^Er+=h(9f@K} z_=y`Jc^eLr7yy!->A+STkthI2tC zJ6~K4$zA$oUJM_}xtL|0mNj+R?Z*$QD#Wau?U$>j<}q+JXM>cURgmI75FZmJZ6R>5 zl{@ajSZ0n9xbPC@E)XT>DkWv+7zy)*=9)rMO9(_f0w)8N`=)KowN_`l1tzTaxTT^s zasiILrDJ?hc8RMo7VS-CnlE{oModZXK-ys-RQCH3QqqAYJxUqJk(U%>de(16HR(yu zvP447_(Tp>evWG-C9yuvaT;jPWz5qh+mG`+5+zink8!_>9D>Nel$_%jI4|`cG*5|C z$w}dAi9Bq3xTkEj>(pg~jW(fNOcn}=md(o&%Sf%V#(X=T0~XnvS>~X_LL3_q?(BdF z=QBsT(s3@(iCO>jaOW5#ri;6{B;=u!C~SF z^e~_&0X+}sRX}e8`Vi3PfW83q9iX2A`Zb{M0sRKhpFsZr{SQoIfN2RZ?E%vjFx>#t z2QY&GGZ-)cnB4>BDP>hRZ5NuDa@+N#hM01xt>?ncbl%wW8Kz}BuIHP~7OU<0(xXY2 zVVIWfxZEDE&u^HP?cCArQN(L>r!uoephcGm#*Ij1$W^RT{axK|OwY z$FSvZBxeJ&upV(-3Z_S2Q<+{PpN*JP`Rq*$v*v6>XH7zQ9VuhN67mh>vCW+Goix&@ zKF%GhqAn~ov*bH>bBvM4qoN=R$->A&6}4L;ZkFKEx=B23Dtf0C91Bq% zbs|jy@a9-km91=bN!r@M9)#aGNr@5m3cy~4fG}BlWS@DF8E`FKFLQS3*GQCqG<$51gB(LW{^2VNO1~;i$bJqI(DZn4UEn`_@L{((E?kv(u+kXWTqS z^O8TYJ+Ei5qulf6gKCkA@2h9AvQ!Zx`Q0r6cOF4cNcp8ee^IXmnVV#b0Sani38X>LF9UA_H%Ed2b?&S7VA9=2@&!lT9wS)ouq} z=N5Nr_3(7o=e=s0*UYuZ3ah>2BVTBj$FQedki>T0}QmSg|8kz=%M#d(l&OXX5YP3A_sw;hl5;ZU1 z#7UASOP(T4+QOGdZ>crJI#v|NOm=_(z&t<*APNzW&>&K<+tR3YsYdQSL!M&fq>!8u zTIE|*6^novGn!ad&LNKW(OA{6JGw?@h*Jw|`Fc-QBrP62Ozst}fbNp}&m>?czzR4F zI0-lpxC*!pcnBB=ya9L*@Co26zz^Uz@DHQ_(i|Xd0n!<+lg_*9wui>O;XR-D$`5|? zPyU6&0_oB;r6_A>>}iGbrX!*M*9pKeJ|~QeRjKK#bEv~>AG7&06L-lb8)tX{5r0OHe(x;b?-1rkZOWImPsx;`1Ojg`ZGS)YEg8 z6BiPb1z9A5a(YfU^CU7;poOBvPtO?wt|pmo4w*=*>GQU@+`VRoVDrU@rCkKc4&UKE z@r_?%|KyHX94=YWJCn^6V4e_B;%Oo&m5N6h@)R>NX|h~vu>0NLT#g29u$|RWXI*u- z_w>J_I68U*-t&pC{NT6PKe=NXv@iYu?wHGiui55_ppeqb`LEuMjvui@NCg_LF4+IF z8;mBi#oF6$(>Suj;dHq@Uf2jFw! zjDyD!x4ME=2VBgoCWjwICqsd9CQG&IWwFI>2c2}$O%F4mYU~Ond%`4=WY9zan;chr z+^4H9TEETz>lzwO7we-d<3EQR8OD;Hszy#5n)uT7()F?H73($Ylh&uPyL+i>Sm$dL zY9^sKt*HX5)tG9!1sW`~!WvE1=a*lX!(`~;3aMopjn*{v7FuMvMr$?O;2&BhiE}s+ z@im|FvI*ykD9X1syxaXf4-0y z-Hn;A89&Wry(XJts%fU*#A)`~=J4Y$K%gMOq%G{jf4&74T0|y9s3lg}VW(a8*lQoF z{l*+{&>@E%anv!#op91Ar=9Vq$6zvBtTwyD>2iC#P0cN>eeSmH9i3g>qnB6L6I=am zE+@SHa5R}ORqL%zZ!rBgJzK0chm*5Qx1&UDs2z2nPLxBrlpi_wXfg7N6*peq`6SFg zWvVoV7MCJzi6xhkpv&}#<_$=7tj|vM;Za^3Q?EgjHKvlC4-ZY*m_|@~F`6V+enkQPq3rYFnrl}%_Ah`8v zB9(X!uyxcyY_N?u^eI4wcxG~#prr8#uY@PRma>47xVQ=*Tw?b9C+2{3hyWu1(D8!< zAi-AAFaxw<1prZicpbJaNBm^mAmZ^YQ)Pc9fct?+2acR@QeMSl*-qfXl^b^+JbCdZ zBIc6|PyIx`Ce>kl>=+&)QWUx9o_~0pcnK7g@y=O8OD9p1WGPamNvF41hD=$qV2~lM~ZZGqbj5T(v?5rC#(IN2XG_x78~1 zbAI&*KiKFOzgq69uYBtpH~dI{a?e_pqckgE^gZCG8c&6KFKe>RF143cR=e+kC!Tuf zkrnaud`@`NTVD4D0C>*qv1eZ8PgmRG8do+JzI6@(vV9)iZxgM$RUKMk9j!|nfNf$A zOx9MiB?LHIZohL0RP7eaU;P)}*UC3m!U7T}Z={TFv#J|J53t zENlIALO%q`#Wl(|9`+An#6Vo zT!>W7WcJ8fxwm2_$iLtT-rcfj8Zb>6M>D}lXXgyft{IzSb7t~fnH$Qjk_i$IiRh;X z11?;@%aeKcAUuJ0H+n*YXX}*X-j~weY{I?&gX8Gq|Gyq>4`Fv2_3)YhO@seo8vflZ zYT9pv#&c}pNgtg1U@zx(FIEOmo_T)_@cyhX2k_n(@WEuEeDmegFHgR=R;AqY_Al`E zY*Ihnqs&ETuTkgg$6P=MBq^fk6sbxo1yFi96;}@E+G-+R7S+mD*AD2e1_5nowC(NA z3INW&*lP>-0O(O#s&ekH-V1fs4g%VHJQdihLXvvY@DQlKknh`Cob)D@13 z{*EIoqyI=nqcpkLhT)1pCZ6}@fEFfg(U3A$IayN{-%@UDvO<72))C1A!LJNEN=<$<5 zX&_?Witzvyk4qy-0RnpTs8-rc3ZpL20iQH_9twx7pv0XCLeFow=cW&Xl zNW0>?8*aMgHcNcBB1$STUD`C&5LZIsaZOY|Gw!(;pEIt=QhCR_-t)e%Ydqijt|sw) z?L7v0L#i}A^;V#dfI>yi(^o(J4KUE4!(evsUbhR%y&LjPqsB)8@b;!Be({z=XWr`a z-Shw79z3<<08DG#AizBNalx*Zx}$WA{#@EjkI^MrNP08bu$1UTEgWs>)T3o#>i8K| zE5BMUaF%-7c*72J;t8?Jj%-p>URhYIN*He%BwVo4WrT1Rk)hN!T-mfni^*iY6{g+# z`?`F=YWs(_=RaFg5)~V?eaHl88S8nE{;Lx+MYS_JD;2patN~B7?V)r!$Qg|_DhK? z_H}kHIhwDi(^BA}_;^ikl@B$u&OX!JBy{xouRpQa6yw;>O5g71r<=jLJd)^j>QGrpsBnp6zm38Ex~b5o>eP{Klp~@T50!y zpSeTCWxtBG9$`N=gX)!Tr9%F`m@K85QpHRoONI_)I;YdzGO{S=-VjTPiYz$l?fxTZ zLy_uhnb(Lei$1YadEh*AIiA*z4LIOB4(^2KYKWsfIW?Dy_b^Cf2LLU8nm8d$5vRCW&KVi$)dV5_5C+Fmpy$u^Z&n%gWeN;o-*10 zOQr1R5Bn~Q&Hd%!$Gkx4e*~Rcn$BH!3i}^s87i&+p(c)IrT*maZ!len6SL^ss?(zH zbHD?+YBidflpR|quOqRaM9@I0WkIhAugKCZQJ|UYy2Gt*q!()Rk0mP9DOInEABjjt zjlr32UNsdlFHI&5+|R&Iw0u=6Xp36)5!o+EkrMTc@|m7y*kV?u8C@cfpMYmb29_|( z=Ac66vo1UBdEe3`m}8zmQfhNoN1saL(3YUMc$o{7Y0ssI2^^##4 literal 0 HcmV?d00001 diff --git a/docs/md _sync/assets/Monaco.woff b/docs/md _sync/assets/Monaco.woff new file mode 100644 index 0000000000000000000000000000000000000000..e468c424971227557730c31799863d7cdf9edf7e GIT binary patch literal 20096 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 Negpu1(g`jA006dq^ArF8 literal 0 HcmV?d00001 diff --git a/docs/md _sync/assets/dmvendor.css b/docs/md _sync/assets/dmvendor.css new file mode 100644 index 00000000..0f72703d --- /dev/null +++ b/docs/md _sync/assets/dmvendor.css @@ -0,0 +1,127 @@ +/*! + * @preserve + * Dank Mono (v1.000) + * This font is subject to its EULA. https://dank.sh + * © 2018–2020 Phil Plückthun. All Rights Reserved. + */ + +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+0000-007F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+0080-1AFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AADTEAAwAAAAAfnAAADR0AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYHrfRocGyAcKgZgAIVSATYCJAOFEAQGBYF0ByAbmH0F3BjqYeMAw/KGq0RUj2pRlAvOVOD/SwI3hmhvqPXWMbaxcFVodQ89qEsVRm4InDI6Gs3VnVb38T+eMKtHpJAeluqjdxfY5AW/2Aji7+hAhpH18LRTMohDxurb7wiNfZL7A7/N3v+faEVwiCAtAlNswsSqkzkr7sxFpbE2F4WxLF0avWJR3uYiEj23E/7//948+9z3axuQActQPCviQYu1JhCmVuh5dt77WUKB5zYb0xj3A62tv5pdfs0/mds72PnOmAWKcrQJvdBLbhG19MIayF60FaCoVEvGmoFB5CpGov1mfcv9SVvt8a3BNzNJWdFTvZnl3HKm8v8lojXlwpz//zTV/86fzr1+pPxq3tFP5bU0YVQJFSCH8NdldEEuuzRFyE+cEobADpqDiltbjmznuokcaPoc3vfmjPrz3/8A1Q2fzsM1NRo9YABbvk9KwBmpuqGbkBNum2Pm9NyEBWCXwrUppZRt/3790tnL7UtoVTiUOSvJ/su1t+m06lrTTe4ZF4uQSDyOQWOs4P/fL9Pev8suydKsK9PxiyhshanNf/8n82dmCYILOFmeZAk+vh/gaZdeYJK/nWUiVSA4Hi3DG+daWWP3VLW2sk5UycoKSbLLnApkQEWbzdO7x+hcD47hVB5kI7ZqP8Ml3IABvF8ggEMBQRDmiWzS+LjJkya7fHqROvwx+vO7Bl5bGHjzwK8YsMMNMWmY4PG85fcwjFcbvqARS3+2OwjIJ4193M0Mz0mhw0U8mPnNbv+UMyb9AwjggApsDmRcePEVLIpRimwFJphunqVKrVdjq70OOabRBZ16XXbdbf967KnnXvrgk6/6pUEAwRACQkboiA222CNAipKReOCFFj8CCSeKWEaTzt8UMoFpLKSUCrZRx14Oc5KztNGLmds84g2fGESGXaPWHmGRVfa2waGO9F9Ocob/8XjP8lKXer23utZ7fNinfMYt7vIV3/K/fuxX/ujv6KBcvFhwCcm58KQXKExey8jKwZujdxiflArhllB2Q7/XPvpuKn5q05C2uMlS5JxnPhmyZSrVbNC8Xa8dew8nSJ4uS+5CJSt0qRv19bhXfexHuC+yKhvHmWCOc57nfBa0+HKrrrPxFnvZ+/AEk0870xzzL7byx+e7L7Sc3hpOOMREzBx8tKQoQHI/6V4QnnxeBGVnVAsOAYkIogJzCM9jDiFynjBwiM/l8y5RU6i/akTP4UWA+trG6wXECKBnipp9SRGgfKeK2Rpe7RisSP9gAznDFqJQsOM9P/FQtn6Xjni2dEET4Qp8wz87Wpx8iqOv5WZl6LftqlNrYOwlHCUmSssZa9yiDo5y2ylGYQUy0G6PxN/PC4NNgjWo+uRLOrIfQ+ulcU9RL9FS2U227AfPjpPsm9ONStCVzViZ3jnbqfv+jEpfmzg7ugVTasuE4QKfRtdf4PaNyS17Z/fLpYBqNVcJxVAgWQlOrdwa0zmjRqehNBAhroo9Le0OUW6DjEGXglh0fwZpRX2/JdIWZWrMs75j2uwglFHPw27UP7CcDNT9IQD3gwXPdF2lPsf4GhNFqbzAsOncxUuuGV9epFYYArSFV1zeAouss5toxjtvO7mvbtVixUPtJl6NLdyDnfseE6/EH9+y8l/Xf8dK/9+ktcmgzTSVnGXIjRLUPJuBi2z1Vbxl1yEktjC5bOE0eNhs5Quiz0aa2hdbfqbFCIAVEDdwoL3FsQqcjjCKKEShRM5jAQ4JBRPbdWQiiALEawKM2HnvEQgYROicVPNWpqy3CFYdD8DQNmC0cMpKtU75k1kFUAKZo8fIo3bL65k2bGJS0U32XfVYwx66fxTvKvyr9uWoPXhpjP5CRn2qazK/lv5vhbz/mycWnnTIPi5YmwhVqURjwdlFZss5ASDw90ARyFdggkJYDvthHGyEXbAEamEbLITpMA1WwhzYAVuhHubDXtgO1bAB9lx1J6REsE6ocqAUFrHTtDRYweZgMRTPoexImyDnS1g1oRwOCweK4CgcgiPCsQanOjw+cAY0WnD2qs3gXNBiugA6hbYGXXAZeuEqXIIrJjO4KVxLw13hVod7V30A+oJ+033w9PcOHg08C+rAWlAJ6epgC+TWsVbwEEpnQDeUH9ADXkBFhgqwBio9QA1oAPugKQUSYUAAhQDlIch0zvDIBd4espgUKxMDJ03pqTjtTH1pEB+Z3cwN+aVidvGktC1zynvVtOoLRVUvrD83s5o9TU+zW5BD/7zg38P+fde/j/zz8L9C/+r9n6v/I/1Pzb82/qs9JENXkUXksVB56JzVwP8m/1vVqazT9k6/d8pZ09exX5dN418wd5rLtMRpu6ZdCfcwdjfeZpxtfN/9l2tObaMKqbF///3f877x+KbnG872WkG+oD/ifZv67dFv7/1P9LvI745+d6/zBSahJlUmwxd6pjtM39pfLBQLi4Sv8q+nW/83839/TP96+obpW6a3MLyYFZPAlDOfhPPY+hn/7BI341XXm2YWznzcbfCsHbNOd794dtWcRXOu9yyc+05+/7zB3tfNb1ngsCB5Qd6Cob49FjYu4vcLXtSyeM3iksV/O09eUrd0xdIjy2YuUy57OviG5aUr6BXxKyaHjjI7bGbwmGvesJJambayd4S3KnJVtcVsCy+LZot3mntWl6wRrvFd02m5zFJ7yeDvb156sdW1yy60zrC+fnkvm59tamwmr+hjG2FbbfshYKJdvt3YVfIqS3u1/emrThsGOPzqMHCNaO2utR+Cx607uv7z9fL1565fsqFogz7stY1c+OObbo52c2xxWulU5LzeeXCc2OXq+C9dY914bhvdctweTvRzb/RY4bHNY6zAR9Qm4ox3eHZ4/r/g7aXx6vTmea/3TvZu8nH0+d2n1edD4oO+R33HZ+zyN/K38E/0z/PXpxQHXJu9WNwVSAbGBJ6dax20P/izlnNwZcjykHMLNoTuC5383x/CdDdYS2qk06WHZf+RyWUTN82UB8k7FEKFXHHqFqPwdeG54TdutYj4NeLJbXaRxyP/LuoeFR915vaB2wdvn7zDJrooxiJGHdNG9ubagAczDDvBZNJp1h406izKxGE8J5+2zJSZMIx3db0RQooCVUMDIYHgRr5CrlLJGRIHlV2PXYT6ht7gPs4gIlUJAYIE/qPmnYsn+35iYrNNFO1AI+PGCRAAX4MDzu6a2x3z2KdfJ49dWtXvI9O3mu58Fj66CdnsGSX9FMpSeNpkIUnZbbhGQMQL16XZeVsYXcYlpniYuG3/F2PvD+YhWuxNORwrGl8N3AThn/uxQEj/uRfzeNJbkYCeXNbaLNM+KC1jerqeVd/tUmtJIz1hg8Ibd8p2ytgdqh1pKuEMXmhfA/b+ZoHcjrLDNui6k6Z63xvuPBabJQsWBh2W5ccwq1adnXr/d6eouuk4DtoaJVonmhhZUTFSOUt+kodSdMf2ZlUVo2ubenbECKL0LtmhUDpEpkmPYcHqRsHIygCLVXKxlZWs+37bqp7PXgj0VCZloQ5vibmiJEGA4jKUGCIyXYEOme/qqjJq6SKBKh3jmPF4Ig9yIQZ5y0OcqdU+Zw7jaLtbCEeNiQy6dq2TQwxmcvMMavzXXS1D7Jq2fle4rmyE6rXknxEjtxCiLWgFQDvTusDW1j9sgZV9efsoI3Yn/Bs+5HfkQ5FVoFff2i2uZrWLq4M/orBlWjXTu/m3DTZtXjvZ3HhvSCPTfcnZdPOshnk0KoauRnpBnbg7hgXX2YI7bc+sWBr9A7JHm2U/PPLTQYbtkcYTTAsOYDaAaeOlmUbyfFVjAyHBwEa+QpGmUjCYGbHfchITtu/djPrNO+qsuuqaCpyvsvHQfCB2YirjOU9aYJvgBAWgYB7yeI6s8Ay3z2fC4OfQNNOhwyCYjgxizTfhAxcjwXuMZAr9LSE1kHZSNj3Y//WZnWsq1xT72TOfEZXFPs0b6Tkh5giM3zTmZeuWdTfeZ8k9erQ1qRNseA4b2jB8b6OqLT9mC95lbj3TeYzI3sr3+1MXpy/eNcUI1lTAHFkSm1KWjszYehMeUUBMQBPTMiKE+9vhuf7u+r7tVqoGaBlWVtsp/ZxYDgpTIYUMJPSCcsg5jGUs5dz/algeu/iGANCDB4EBOziMUcaGLUeMsWGPgcUAZQZ1DlaI0vtyozOFWvHpo01pVUzRYSVx0udnl4cwS3hH5guqTpzNK9W/RrHSuyJkFNUWYtFk68kmn7CpAJpESK+oQvwW62A9fFM2SvQKGt+bamlOXB8Zd3T5fk3vqHGv6vyZCdUXqr+tym75PlYkUsb4suTWNGIbLRxptrO4TkhiZTOUSGvkNCguRQUniULeRHZcUFqGjArjLEjCHluPMacotI/nsua7AII0/iCMTzeGHgVEuUuhhzNJV0K4SZ8UQP6SSFVCnJvE8ySOfX6tBWqi6LprCAIfZUhUgmlhJ5g2duImpa/zjKtqdoPFUBbmUuvcpRyJq+hJeU2r6pZa5wTlzz9fIurmwjvOZF4qspwK4dYeEpuERlB9Yi9BqcryEqsOlG+zMhkhq67du5yfTmoBcXwHPnyddlWmW7e0Th+9qnnMhVt9TG5d9p08P1QVqpRvsDZiw1SYXFijG417RMgHp7AZ2vGZc4rAc7ufYQzSrWjd5s7iig+Qe3jlGekGT+xY7cLP8/V8DXR3s+RBgAmOJw8FPMGR8vEy2FQbx+3HcO4+Img4Ybtxn1Ms1VKuAo9sxefpuH7qlLh08Q9GIX81s33VV3zmpCJQMgQJDCygUd/UjynYzX+I9YB3JwINWxOFbMMN9g6aEIeJky21Kxso1CPENZCJS41LSUo2ksKh1f08WfiFmTjN1LZ15zZ00V2iho2igARFKtO0maj+yiHxgvbiXSXr93to96qXRc31DHFVGNZUzrqA2G3uoxrQ6yga3/Hz+ZXx1eHZLLVdU6c0ONz0udEnncJCtyZvY8oSiLyPzUIPaKg7GYlQprizqeE8KlNT+HzHj6kSWbwmIDD9YPkKG5tCUCpNRPHJ7Uv5GM1fCRLhNml+Vzaldm+GuzdTmKJg+Q2HE2Jiestj3G0ww08USGB2f+hqy6h91LCyqryKod6/2L6sgyFEzXj95RHrfRk/ma9sjhB5RyaDA/B/ruj9hvHqIKZF26a40RTnOLdm1MNK3UyNqny5QqWSK/JVDQ16EoAkrPDDggt+2XiIL8Q2YM4NAhtmMotZ5UHAAgtsZpW19W1hCzbYczqJkA+jP3S5fsF79oWPKv+vVwcgMgLGrEQK0L2wAa3vEDt5qBLuAkiN890DUybp+uqwk2AZVPLPnfPWr3cKncGSzwwYWYMFYtuu9v/8aeOyOUrIvH4OenmGfW36iARtaW1d9TovUcSWlAAFE50cnRwg9GpWdaQw7Sk9ibZC2TZ/l5SY/TlrTKQLoappP2qhAVfddPLt3HqbRtavZFtOAV1epLbqYH1p3HUf/X3MJNNjZLIXCbuQXW/hdMbWRJQRVJQ/R8aMx8wokKqkm5mGzUFF7vQG/zjXOLbbnTj/SznJgOsfR6KLtyMra8dWegYSC3FE36vjX9atLel8zboVVM3ezs+u6k7bULjWmRmbEudBuyXXAN3ToYO2fumPSg91AuIvL1bFWMd/aHy6NFZBR8RWXNy9uvzvhwfwrRSRyREY9eNA8mu4ohVflZuuQA2h99mh86GuQPpmTHydAYqE+nJAh92N1ZeH9g2+0r5jQ3HKftMol8ZM7cM9saarN5QHAKk5M/HapW8tN6VaT88VV22g0bPnPHT3YAvi6Ma2213a2FV85vb3sV1sozvzo6n5uKAWsrbWQNOTkuBSBB7HjAKSZLA6v9D8hWt8g7FVlp7SbiM+wrnJ8i89/m7+8a5LerEGnJykTpApE4+WbTJSPyKkoXq7hR79ra7F05iEpACcWJFwiA1MYnYUSW0iKdXFYBah5MhYHY4oFKtpt8NbOzD9mPvzac0j3LKPZbCPUelFBTyxDsFqxXrx045Fz94Eq7xGdaV46fj8lrvP50GreLVwaw6tp+xJoFExFWW82YPxeEqHPluMS3n+A0ttrCNfYA7mL3rz3gslB3RqvfVDFqMaoW6y8VGXUXu6W50nPccJSRzHsTuZdObCBmPCi8JyTt1/J7++rLT0FRvcRSxITNsuptduKQGGVw0Ly/kYc9sQw7gwpa+aQUEQT37xOsgh//7+lsp185HDdgvmebGwU2dS57Ex1HioFpB8daEmFtIiZeMwFxJQM8dA9LgrnD2BD+QXnaBYxv2knmHBReceYuxjiKXNA7fcyQ8ZFowW+OKFDV/Dotsie+ZrV6desBhGx5FHGjFMFo1wym9CNwQcO1jPmzvT7IPJjsb4mqNFuO31SKy38dTg6Un97gPedTxRdLSXKLzubOw6nkV/4lmsvWauPBXP+57U5rXAX/s5xm0DbZuNR9pMO8vb2rvmtTk6Boc6OXaGXffWfnVHSnmd32D1ePXPnPy9UUayJI+vt3Buu62z2Jp+yMz4cTOtTjpQTEjIFkE+vNigJGm4UNwW21VkFqoskDSS+ldvv5RhDKiOcT2hwhwlUZByUzSrvYXo+tqqtIHSowfbclmqNiOvcLOD7lvfEzcgvzqiMC1eFUOrs/YWrLCU5/H01HwxTZraos8hCH027hkOnRUgQ5A4TnAFu9v4tUWyJF4lyqiovMcEzQIHlY2U6qzAj0kTOTT5BLXRxM0zFLUUTDxQFd9WejlrTP8EoT1KNKbsLWBhmgWSDhtS40dJe44cFNJ7dmMWD1X4f0Glu7RfKy4kZwJRQl1569DoYRP1Zt+ZO0KNfvOvWVeQ5DWYYMHZDhvlPJfq1HnZtZbPOj7s2ssSiujKzcfi4jZvjtu2O2vfT3BLWIL3Lz9Q7UYuVzDLSoDsno7ytAUayonTpwO9CKE0ZRlrlFJvYElI0tQXff5m4Apb15uF5F6Hw38MO+4lSEZMkwVEh9ncDBNNBmJuQ80tHMmUOyFtQcJqAMPeZfZSEoY5OrEUYQ5peFCyEmHsAmb4BZgYD6ZriYpJoyhveqLEkJ0lN5usb4Q7XeD4xhR0pvQi35jSgDRCSS9gigPrhau5zFMcneApxuQwVfA/OZODTYZH2TxX+nbIODxWkL5Cs8JsFc4qVDQ/NuKetjRFoDh2BLKOeb1Df2Aprv2rgth9FLxLL7IuhiE5k8BMzSLqvUVWzV280NN3FosqNAt1PmZnpctZRPm9lrZhUUj1SN8Y/eEwZMot5NYYE+Fv4eZNmZQxYrsM+Wkz2v9c442JVvN4Xm60Y8edS0vep/sIo4I+J6BLyVdnW2MBVTbQCBeh+Bt+QaPiRhTjmwoiCo9BxkAG0XeMkYaDOZdxNBmocuBYc/szgb8M+Vqd6E1wseEhhX7lzqayhyKBeB+GN+iXbja+aZcli7GB0JSlPjvOd8UGAvdL15IjxPmh5AiOZnC23trUgYEOW8frYMKpl8FUgfgAIQg9YemBVajvBioO4eCeqOB6XnN8qAfCvJ0jxLSCFxiLldvPWgSIdV24hIumLgqgFaUMO2XhhLJBiHDhQoyA3RiQCoIwdVf2QsQpx+YgSZY+pjcVff5Q2dmIwk7xwwrmGPc4YAn+qTuhTzs6jBfcHuAcjYvzQ77/8kIkKxBsiIjPEZ2scDW4xJicOC+kuXC8l1sQhsAdsD/gPgDSiB6Rbm1zn1zGxI1MyGsTyzI14K4Dbl4OUcY0kD8H+cMx7WZ7Wf8QpM0Cft6IKZMI+Qbx4RmeUOhtd5dORJRBBHpziNMu3CS9KW54I7lXv3GG8EJ+BtvSOJCx6jIULbXkiMdpevh9JX6LXRiINRTU9Yx8STc8SoatfTH+vybBKMIX1W5DF9Gnoz+XHs++1F1QnDPJEK6k/5NpHVUwN9tHjYmmTApXshKxoNdCD6d+T1nhoSpgETRDJU93T1+42DMIiR7pK/1LWQlyGx8JrMNWR0lehy0OHqkgIs5DA7dzA/c6EWhPT1k4ikKj0wFBfi5dQScYnwo2iMh9NcEBZeMA+oj7FPokdujMB6YecIdaLjGh7N8SXjsMjYI245a1/pMQRhxkFC5JZqnuqITL/z0EDiFRMIx8OzT0M4YKeRAfxmf35Oq5GBrlFkVQMGMDIk8zXJgEwjOdLmgfNajfFIBt+TXgPo4ZQiXCZzP6wI0KZ7+ht9i7nQ4IxgR81jexc44K6NBek9sfO4/Rdmg34TtDeOzHPXXQUyLtwykZLfI/x0lllxsTYYpE7cHeUD7FYLdCt4Ty0wcd0Ech+TL3tWHOcUd4huEEcYNAdskWX4Y1zFeS2jZltevblNv2+OfP61XyjrrOLW0+xMCOgJWd9uK6Evb057UPrVEwruhjNgv7W6B9TBfEERcnn5d1/Pm9TGq70t1nISPZ2MID7PPrau4TuGRjQEn7IktypsH5kIehsc0thcbyN7iuDeukK31rmforRIshIc+M9xTK5y+xQDmHak5nsx+IkK8fxVX4xBOcaneXwjfdSlc6ydzUbONPhs1CZg2v9M59aXFCw0yl2dJNGvyq1V/PP7jzim6sYUu7XinhPxxobBsjk33Ga7uVOYpLR+2Mtidc1MKB8IVfKwxYgMVxMLAP+1yMAbwk9vw7ASS+3crQTvFOn2Ymx7/N/mktQP8SGWLd6JlSIMCnhCV9bfb/egQvbaZ27zpyh2T8uglvZsXy+p1Jh9LaeqMak7BLDcz6mv/XSyCoy5X7LZd3bJ+7N/krb/l1FifET4fNvJBwhMBHfunATgRlEZ0uFV99bBfJICLiDRFidx74R8BDW+/gkXvZdfla05di5uPumMEstN389IYen9wYG5OTWWVxFS7ZsUeijkZgdjC3M5nsu5p7GE4rt3BAm2MY3jk2y+RdNS1bXfSF6m/b3cgl99azjeDYFWfcn+UMwePocUOwpPICYhMS3Wlnl8p7D7GUlON7x+oXj+EpB26CNlOM1UHH0Suqs3U4VwNjgs7qMrzDW8vUBiTE7Ac8mbyjs1PWrmN4cszGw6QAPE9bBGIQ1R2sWMNevZHO0Rx3csiBp7frDnV3HYEjJDXZ0z9qhrKMZROt57KkLk1ZELXY27SZEuNBaYsuRR5fDXXvSw/9/h8NejLyg5QTKh7RxgYro/Ba2dBzJkGJvK3pTNHlfHictlyABEQOej80ghPGmmGKZ1gvlV5PICrrBePE1YuZzfxrFs1eHVhy8UGGTA7ruIy1qwPdHfYuNBHGnRr6FxRQnLF0Qkz2wbs3dDNvCO9lHD4r2Lk34jnagwqMpxdbr0ExO062WrZUNA4pRBW/HfNCHz7GVaQs7Ff2mYFXCAx8VfikFqLG+0qQKMjkPV1SlJdh/LIdZzLuWk6q22gr+36DxCuFy+XRZojNPnUghv+G8EvJukNxOMOxIQ11jBpofxAoM8ExRj49rMPVUxnGnTrIFZyV21wG5wn/2dcdzjGMf/SoP1+TJHg2HUYqcuZ/HPpbTx8bVHnYKAfsFg9AEaU6Hy56/tNEbCjvXvvN8xYJ4TrPJWiU7sjBz05v2/mJHarY7/5wtI6RnGlqgp7HCtK5+0XG3VTPQa8W90oqDA4XC6hd+JeSrh0YJfgtugtrbI5qXzlfci0W5703areHnxAT5yxE9FGGWxE3FMvuR2T3kRWFlDimSioMt+1kDPqnGVslmJdqeui+UtfZlREllltjgXGiiw4x+KoeYhQccSBU7CkmQ7GD1SrAxBOGRXQZg87HopkHnZCfsnRTxBpo66dLOcOJHY3BHJ7gW3wuUKDvO2A6thcQpWfAHTMAyiBc0JdJju4jFy8TZLGxYaRSdS8DbGVum8NCiv5DvqDajd94D1JHYWpCNkqsdmTFG7G4tc471E6st2fFyoVmX5L2/SZEYElEw1ZGw0GGJv2KPELkIaL5IpcRPv43HbVRXfImDelOhfCgZHyvT81zzjlvvYV5AFhg7j+2iF2UTRRorPysaSSNBRwykbsXXGC7ds5xOc4+/Zo4dD9ijV3yUcNEDuY1nNnAUoy4jbXkexrFfIIyFvbSaJ39OCtNft82ku8doq7ou4vIA9enIrTGN2eSsDP/dTvtrLoDquHiynPWH5zIRJNSZNg3T3DhcqRn4HfTN8IicU73CxoOmndDEMt+pDPmJJiiffeacB0VenfymZHf4/IN0rrhAA1TSpyF/t4Prtc9JU27AO9cGJolfWm+SdOpGWUSY4nBfnymFiWUhuOhF+RHkQ7mCbafDECa9qNDCR+pr9Fbxhc3uTXJ8T9vdurfKuxmFBDatw2FDrr9WdjHuGvx4JLs5Ri8dMmGjtcCfSkugU0x0kaa4XFSsHtRvJDgmiQwVAyRc9QJBzMjzryzcw9+ftLi5KdIbZl8Y/K1eZqu0EI2r3xxFt+29BxlZ6Cgma+mgU1mz09dVpi8Pf7H7cxHS6OAqivg2fki22gPH6bbnbi0fcuibvD6xhUMXgn/9cfSdyTTOJWJk5Hg3Y+kMt9A0OJ8fEvgcv74UJt4R9rRp+K896VefGA8g7IT7pK0TdFdLCBZ4hHH8gTyUAgNkSa8yVUNYWqkbgpv/qTHwhDH0ujw8JJOsYw84ci4A1t1lQmlkavFksa+HRI/xmqOE3f05aVEnhtXpmZcszzjJMHd7ibqzGDGrSBLC1leXSGyeEjHZaybebFE0vbNeQcMkDeA8xW7MIe4OtM33IpGmHYTXCjKHwFSPbc6nzCyrnLk3vmXmfCl4n8RCjiEXXf2BfUBlkRZIxIarEybnUEZoep1AEahzB9v+vniNm/2JWeiq59aTM/fOHBmlcepxOufYoWCBvebQciJ7D9kBsbG7n0lAPLbax6Az3/TMgnR170/1QQ26hmDlFZqA2zC2/lV/+shYQ0mEJ161wVxA/SAckBSTFd4A3hchOYpC3irTNEdzCCHzeRjo3mn6nbzntZXY6Drq8sV2nsCNKWIwxMtpEUai9amnkof48r2jNLtjubxwFqy20+gTfNqYS/qKvIHe/kdYfFQvHnMeG4D8yVVGQcv6HS9bXRKenEuEljMLdkqKQCH+n8zeJ5hqX+zLmBnVhuVGF+GhXq00CNHyCC2i+OPAf15ThcrfbEtqT8PaVKp97cjW/Xdks6ZAS24t+pG5S6E1qTg2p5fzvzAUvF2QYO3Bm5N3OAs3VxnhTLKg17qcv8PD/1DuEfyyAPMhk0MwTwzL2Xbbn4P22gPjkTxMa7yjOWaMWtutTkIYWw58iKOi5hQjMcJI0zS0ZcQYZi25+LWw3tI5y7UCBCt5j65Y5/dG+ADqBNeCqgjmqcPOh0DjoWm795kKTUJmiQjnzRlbDD980+YfYYynYnfP1PcSsHwxubfS9mcGqVP4aNeFxlkH8CCrz6QBvCeXOWK0WMxOAEjr4O4Jz+SLuKVMqeDqxS75PXRG7RJLzV3QGgorvLLZUz+o7SyoeFcgrXy9qazj13JhxscPMchl/sneUpZOBj/ewznwo8prJV7ll5eeSv/aTwsxaVnH2DdzE3hfjvn9BgmLeTiLbk1zx6qbz/U5ntlGyW/B7Dpt7wvuj7WbYkmim8JyfGnfbQFesDhPErv+qFzIOqebeCKnw4TSxnMCe4TVusNTum0Q7bzhO7aWG+KPlywzIzaXdiFI4TY0vjV2YwNetfcMgK989y3zgs3GMsbh9w6Ma6HHYZuYp016p2P7ondG0kP7fl6IGY9JdZtPVMfHdbLq19yeaqBZ+cKyhRHPNcLB/hkmZq2+8A2A9Uxq38k1dxUwDf/a6IXObr6QJtNnPUzKPUEJaQUGvMQL4JYo81ciIHGUafLEEEoYWeLTgwNRhDG6Ioc83S5483qUaRnIoLs4qcfe7gHNOAe2wN0iYc4Y92omdlMNmkK0Y9PBt8cvgcJeoWVxHOOajik/MRbzLofngcBmkNBf70Qruj2aZiWA5nix6aShiseJAWgMm5qBl7EJzR1SX/dt3qorxCULUMChf3n8+2gpRSetNoKjn4bQaWEtgUcbMee4YDwbG/+VAxx39BPwrpyMOdy1rQZM1mxfge7nfsKizNoanRgeBNpbl4YlzMJpWUETc8BVpVD6Q/H6+zv1gJUP+NrtZzGACxu6g1UPLIgnats+K26AGxKMoyvK7xW7prraCNOOD10Ady2V5SeDEEqQ5pH8e5fUpWr9xJ8XFYV+pfY0Og2aBZGelCOQrpVd9OmWhbXyqlJ4kRLevbaF+CDvbP1hNBtQT4KSARN1RJvD7kL1R60+p5ezXg98k3V3CTiB+RN52omQjGXbsOrpIC3RIF8mVLyA1fntJJ90EVcuzIJMCVrnNDs8pI8YMxUoD1GQ4gw3weOkJzNx6c0OocD7qNxcQeLEQ7NGUseImK9vDkBMRLtrrREhK0nACAbUxtAA7d2RbLjmdiuLY4atbbGksZoprGWt3tA0avMYVq7G/m23jbBrANHpuHRrOm/PpUBKIzhS62jcF1b0bmVdtQtacwgpMjR4uj5qzaVdA3XlV3k2/Tqtvfr6nhkLxCPWyOX+Aa+Fq5w5lGTW2/yHpNvrS6D56E7ex9jHBy9A1w29W6/xbGWxQjpHesS40TP2qQ782N3c4fHUjjpgT59wyF1ybRSJVhqwLowAbfXYp3b+uwl9JMsnD5KFKz+7D2Xww/r1anCshhSQr06j0mpTlrrd/hGSihRQkaLaKEsiliuN2RhCgyT/Zbw/SX0FWkBFDLGpVDm8TlCHEo39IgenUMb7eCAn/kpJBcVZ4zCGH50lbu1ffy8xPov/b420sNvr2b5KPvYkASGmsRu6Y7F3yH21XZdntu22VQCDqZ7Bt3D3UlwsodAYZwfuBwLuV9ClcOMjaeCUpXjxSmkagVceFVKsiwhMMP+rGTcf4nkn+f8H4SafLog3b74O0TkCT42VNQ2pVDaS0rSCNTwOo8jAJvmneZ4sFRTPknobaNY+3DEqo0D63PGD224oDHbBs/2xqEFI3Ax3/sm8nwG0pqmMZD9zBQMgVfCi+CwGC6GAGZyv8RbOhKrVAYBCrax/9U12QiJFIdES12zXF5DOfLFTG2/r80WCGY3Te1WmCNLPmeqeb0DO8F3cAUnUePuPsG38RyHi5Yv/trUbhX/obayUwBxbuFSvj49/DGz296SIh4O2OJvY9SJcuWnEp8LJYJ2pskpMulale1YSfl7NoO59eDSjyNK0wMIrjTohjgfo0TjiFY/0/GjCyCeFnyQFiaoltMpnih/xZX9EOVUtNUfC+dR8AropgtvdYEjaD0EuLi668jXsYTfAFxHmktgcxE6d5Uf9NPzkjDzn7bXqU0Kcu7/eLeV6nNIEJe7uapSrXawgU29BGS1Wo9+nIfsglHRer40L8Ybal5i0dGSRhIqNT/qkWgII4SKjE+jGJNSCK9eE/DI8z5iMYjSy6w6eWyQ51JZgjHEYCsWEssiMYYr5EBcXz3oIJKSn8CrVoqe9B2hFtSRCvJxpRSHnagcoYm79lnkcUvEwoUslgrtyzyQuT+zNMzkzKCKgTcLX4mVU5/KDYuVHLwQXYr3CzMLtxcYHd9TswPkaNVCdzFcLGBxXPAYzFLq62mJoQjNYhXCgqwHu7LFxssU4YGs+eTcyuv535Sd75Wx2lbny3GqnSpaYvazvYLNPhAWSCAWTuCZD1HDpvD+j+oXDVMnkKCDgYFKPLlBNOUIc7n+kqb+CLdQ9AwVkJ/QS7xJJHF9XbUGxq+/5EQ+FJUFfvkApycGqE4Mw9xjJSPuZLidFIDq7B14gXPdWRZ+zVJSdPjK/VQ62lhT/lHgel5brvbqYc9sdty8yzda1C+lUdx3JTKYeVIP5UPsu7mbb1otXLRJjLjLvIp7phhPMUF5utQ/vvUjD07naoDbD7p+PbrJgnUGhzM2/kOGup8ILG6jey4vvV+7Sh3Yzpza4lTfaF+1lEbecMd55jdmgf+d7LPHyzdHCL0dg2wdRbVXpzPeXoTN+aab3vAgONh49umUVlR9w4KVnkROXtSQ0QPEFkXyksr0k91u6e0Ar5MCXnWU2oBdjZaonMCzbCFUPfdGheQs4c7hAjQFnYJ07QrjVhFw5c6hZ0x1MslWv/QmDRmpNZP1a+8JsQho86eiyacn8vKTpr2EmPdJNsbp/SCHmQ907b5J93tbxqPKLS1jCxSUdBnJNtTvijk/MiFaUoLNwet23PpXbxeG3RO/p4mQJ6fQ3LMl6l6mkTqo6SeYeZRHUidHc8+e5z7Qb+U+2OMS/vCnmYY4nSO75tK4fLXquL/LPX2NGOZj8poyAJrLd0FtF3PWKmxQY+dJeGRqrEncznZP1gDPqKaP0nM1DtI9+hncWA1N/CpdJPkchzZ1Sa7t2NjaSQU0YoC0O4nPLJK1oQCffaxLX4TS5mSTNg2ijFnaMvZom7DBpXJ9VL6vgT7mQwPH1diU+meguOIQqESYNH4TZnuoFPLjz28funCrTkuV+0Cr3kRULeatNLDWY0JsRKw6cwH8JR92du1E/oqZRs3CTdIiYTXHQl50NY91j+PTm1EWE4HfI4PFfXkjeUn7ZBt39Mm7DWRPwlPoog0Pl6wVbVXaStlb9SPivHU4itxVa+FWemnhcO0KizuW8pXDKJ+HUegQ+W5q65PvINzjs5WW4bPvvB8GYZnL7+JJmI55LMTJZ5i0VNpMiSE0pJrE/CtNMcvWb4t1B2FdkOmwnSa4nC3i4Orpa9dF6udXLmDANgepaXYUiMYBp8Q+gGPYfybscyWQBAfxNge25kmiTbSlOYavztcSz7Wvkw5cE3BP8osg2AHEf+CoD4HjWk/N+xfyq2KGIESAcDiYiFmNw+NnhRAJreWs/ZncaTauoe8sTeZ+CyHKxibOJf1+qbasPr+2Y9WriOn/iThSaFe/np67yA65r7FTuyVK8ii/M5fnlpIsUybmnEfIxU3Sv4JPzLHgeYDccgmMjBUVzSO2gYhwhG+0eFUsuMmAqCjtvdIWSpVpo8cRYhgKM0WAudBwEZjMPCX3XAEBVJ12NSHcMq0eEKQ3FgSnaPJGhHhVmiLhjfaJdQsw3oEYlG9deJfkCH7p7LhXAASMc3CcPM9Ck/Siggwsb72WQiJqibqpK+NTXvKl53k3z/rBjDXPlF/lc0OeWEeEUK7m6NyruRo3KpTvapDEQngGnA0edDx/qKlp+I+OgXdz+pG6emJgFwVmJX2t4YHXW4e4ECVDXX2jUGoWSilW19MyNUE1QnN1qasPCoba6ljcRzQZlCtdfranC8Qm2/EMIVHZSNRLycFbdJa1krmvv7jyBK8+vfzpata+mpiSEpwSRnv6Nes8eENZzcoGlKS5BIWHO8OEwXb8UBUKU5mIsDqeYK3hOuld0NhrgpAU91Y48+c4IYg6kWL2ntgpp/fswSw+qt+nMZ37rzXn8mORpPx5/9vZn46aqNK9EXlu/O8/4mP2oVFEM/WlOSLUrgrTjikaquR6fBXilKGMYRvK9nNj33GEUkJ7zUlTUGMOeJp36ycmOaUB9KalwWE6ac8go8xrzpl9MCRFuIOT/TW3TU1Ze9HvPq2QUquxKkrt84wUPsNfDFbEprzgOEKC7mA25zSKopETalokPiRZFqGpGabTBSxhlM0LWRpAgfGcd9UYz4+n2N69WACUYamKKKImogvapguwLyuagUbN7LGEuKAeDHs6oHT7Exx5p2/5eKu2O8h3a+JDUuSRsk1d67eEENVU9Nzmjt01F1Q8m0NhgIk24pOgpPKm0j/mP4wsDKRF7h3nY2edn/stHfBi+ZibxeuM23PtWJEKyZ0Rn3Hb/dbtFY2/L1wrojm/SsSKLwiHHTAy9ZJPBp2knEtXKy1cuAGTcudhW1phldQncBYfHgSxxkcBZx6SXExw0VNtcX2SpWN8cyYG+YUhzSFYDConyF5PpAvfk/Y5CuTvYE5Lb1AfHmPp7rSm8+8oiUfG6FKVTUFkMkbHYh2FyNvjcflhlkD/HKMMS5vSL9JpfObALOeyaAJizIkG6BTiiRM//ChjULxANpdgoRcKPyxhR2qk1B6sDankmjNp6RP2Trx4vcgsJJdLpaxjxQbRU7/W7PWYyoeEem13x6LDatdohKuBBjAO7kHFcM0L4hMc9HxPhIYaQ+IfXJsM2IHcGGUZ/2akM9Kbojj0gQsdR9fERtSxSkN6Pba8IOkpKih6a1gO78JUbLghDHB1S/QWzoV00YfPFbK5prUsh8DlkcieMimZcepcvhqGkjyXzD7yXpRQ4U+893T1MmXjUbHgSuWSkyEk6XadredfKPQrBcf8gLFCPe/tN224fqksSA5IXMOfHdR6/AJkwG9ilOheBqb7ZKqZRKj3lpRpqTDnTvFYC1Ai4OPzjO4NLR2CveOnDkb2fXu00KpsjdNhF08gu7GEoFawqaknlqaXu0m5lIJXcbIfnSkca7T0jKyNcjOWLndA6TIH+EfRJRjIPvIX3JHlr0KmNgphUybjxdO8m9bhDQW8yzgeB6u57NNmwsnu1mNulj7LD19yLY7UOgx8qV5OSwLkVX9beKDztfaUe8LDIGKKm5zwnOvWCTJgNIzpss4eAn4hI2AJ11/9SvsbnqXMh3L5RWhNPh9hSFaWQ5PwxWryjbqjuXEudvTEoJRG67jYXbmhvKqBRh9UsB0A8JWbEACgwcGCIkOKiIFUIMx9bhTgE0BCT2MTr2q4P4rph8PDXCqngI4bY8LZjElgB8lAhzj0KBuEt2+D0dDrAkgfF6RBHiXrqlQzRjRHaGVavbHn3yLYESAbBJUi1/IJ6YNFcLMFtg8Am9N749Dm8z+TAR74TPY5o0W3a175nDt+jOYfipjKdBZTxm0+Mqj70Sc85bme52mv5y7eQ7goo42Zgs5KhJVDQEJKTrdev56vJoRDQtYFNz32mlXP/yW+4Zs/957fz2u9V2/gTE5w8lOfx2mv+y5eH/CjjpmctxX4DOx8IlIyChwcTb+UydI0s4+pZ6qZ1fZiu63I1JRFjC9w98q//h/Lvc9l/TUrF5cbmVPllDlZjpnDB2eDI8Hh3IZGAfD7Wd689rfp905AADYyFcA0AAsA7AaIXIAYNSAzO5hM+FtDVxkSrl8vKEBFgkQDOjDACvEnQNayMdlg5adxxptgokkmG+OoueaZb4GFFuEyWWyJpZZZzh7PCisVK1GqDJ+DSlWq1dhoE4Fam22x1Tbb7bRDnXoNdsXGVrvtsdc+B+x30CGHw8MRxxx3wkmnNGqKhY1OO+uMc867oFlLdIQGrdq069AZE2t16dbTCDjqNTYGYgU3IyFEhAC33HbHXff0RUZwIPSv+x7o98hDIuLs4eljwVAQKhJCKGGEI8cJBUqECODjgBgREqTolSIVAgAIwK/94kGJld9bieGOCgAePF0X9Kt+u1qGc39UJ9NNAWCA5soaWFemkdZsY0Uuz5IVElHnLVqlhKgQKZmBQ3ylKBMoT7lwp6VJ1yYsKnwynZKCUpbNckwnoDdWIS2RNHvUqYsKd3PSJIdCzFLn2U67CHuY3Y511vqjTzJwDu7pD/KJFM4a0Jp+cVshOZTT5KfVhVA5PVtpF+54sBaQlPdnLy8DACkPLA0hhHagVT5qm90VqKPZC5VeyFePm6oeVIExyF5lKj1mUEDLzz5UHM1o5qAsFYKt+NZCGYJ+yjGn5cTdCD5wylYKe7uztyK/rkDdGHFFCs0Ne7Kq8qLx7acVVK9+R7hQKRByWP6/uoyNqTwhbSwh4h/s7CQHjWREdBAMukW8ON/btnN16P3hkRnh2CqyI87Ix4GG8+eH2y1dep0FUOIvW4C/2cMOSIQAiJEwDBAcmfLNA5cjgIN5l6OAwqLLMQBYEhkH1xAP9m42As7ahZhsirmmGavIGDOI7NM9enDjTndYJqTCRHLN5UCjMfW2Cb1vd/ytPOPNiIfNNImaIQhBiKnapttYCmymwDKLAvOpJbO4VjJ73+zNx5psEhF3m1vngwEMb1gFHgYAAAA=) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+1B00-218F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+2190-21FF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAUAAAsAAAAAB0AAAAS1AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYgTGyAcKgZgADQBNgIkAxgEBgWBdAcgG3wGEZWcfyE+EtMN9vKA5Bc0yUoz5//3zdWt7i7MMhk89Jh5YNQyuHYEC0QOBNNZU+uBFf/fX3vNFsIYcE02IK5Pv98Efw+vn0T3zFEboYh4FE8cSTQSMomQ8Ug0TyqZBNtCn6ltmWJarrMX4mswKBIYY7H7bxbmnSgqLFpx8yIZ/cA9xgIYCyNgLJKMxbKRoIjYCNlyHxrXrZvx+abPHxHcKkZSgdCXUgHlHqe3oz//PyeO3o9FFGNhscnzV2zcsf9YxvXRZ1+7WVpaVOU31l5RbOUBkheyMMmlLEZKVK6tsKIwZ+2aLRsVZRNls3HL+pRnzcrVfUmk4lcW376y5PCf7C3gZLvzqq35t/M+nUD5ywykaFPZH9GRJYNONVRVTftcgLGWUIRLYJCwSaGn8CfbxwbYj5JJypDaol6kJ31kizO5PqQxs+bUnM4k56Br0Gkht7C9/jE3vSeO0BJupOuTZ4LoOC298SmP8tF9cfSWHk8L6T4zLaXjfPKCbpwNpiXiyMevCxu5WT2DTtfw9clOp6Y504RNeHjUi8YuUazT0k/jTC76zsgym27QcbGUT545YKQHiSNiCTe5Xk8nN9n4YN9gb19Sn6a9FuFRvaI6Ko0/qhi9pcv0xqB5ibQ4a3Jf7+BgXxrZyJ3+ulgijnDj7IHJC8FiKR0/dYNH/UqVVt1FR+JMrtdjiDg0F2uofsDBPCq2p+4WkcQ4/Rc0CYQ8BEJHaTsf6B/qSenV+i0mVZygWm+cwgtQpJC4kChANpOaHAGRNzFs+v1rfGz+9AbNpvl8qi5YSM0pQrJV3ORaf29oWT1DA/2Ila5ZzAIHJrSkNB8rKfm9uOm9AUf9kDPZ6dI0R1rUF8YxRkHGSZlyab65jFykC/2tt8U39C3Zy7vHxdiD3N3scyc7HF63O6211dvU1OhvqE96WIxzBzb42zglGy8bF8z+Hn93V1Knt7OlzRJwO/wO1v8WY2Xde8n+gPhGfPvWRdJJLxOucWcdzTC2mB9UWrFUcwkjRWoEg6dhL7EO5IEOXyC5v98fCKS1t/s73O/Y4+22lNA4D/tSwcTNDd76pqRGf3Nbi8Ud6Pf2M2uo+5MN48JeTt/Qt5feFrrQHyTX3j4epQ1O/Da0a0hUDgWJWx3B2qDxh49Ezg2FiEJfny9UD/swXA8EAi9OzIuI+LCvvSvQ0f1QROTE4lmpAO8ygwEIQwoLtv/g8TMIgQIAMzMAABD2QotJtUXl3ywvRAIQIlGRnmMB8EkROUL6DnCpsewhrM059FDtivfmBOYMyI9OE1B7WOhuBbs2gbRVjgCgUgBIAGQAs8hgClGgQgUw35zPzEgw+x8lEC2E3C71ywOzS65Hb/83RFaeA4Bfaz/+dHJy/dvvzkxNU+hLyj2ADIkxRf54muAPswIYM39VAEN/eyZ1I0OSc9Y7J0auUtmylYIVwgEE4xIZTAkFUAuzXAYFtVwJEhxcGYCrXIFxOZK49EEsm+9TpFiNUjmyZCunehyqtVZbY3NNM7KRV3UL6lFZbUCOfLVsvkm35SmXrUKhlVR75ANKHNWXEbR3JuEdbaX12qwcQKftHPKwMjmKFFKtsdJqwZQBMWaGrQUAAAA=) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+F8FF-10FFFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJ0AAsAAAAAA/wAAAIrAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYIRGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzYDQC4D7AZ5j5pUbZm4E4qID3FTshVsW6OPY3WC/+/H2rnvv11ENDEkTQxJq+WNZLJJVAvFm3aTBlvR0kSNfr+2Z6qhKJBM0ol5Y6ja8JLIZEphKPV9HOFkV4XMJnQRxziiAWnTbbybh5rcbQEVaSD4FH+CudcDRKD1sl994/9u8i2QA733nQ5oNDZoniWWYBj3EluDplDkEWft9AMzuudgMIgSdgg3JGVZ6nd2Bv1B6OwinfywmQofJcRH0ykzg9Iyv2VCRnk43/6X4PXUhMnvZ0DScRl4PMKMu/dGkVLCpfSOfGPKU4mFo9Fou1nWW8n6duj0Rd30ilt5v1vpvG4gDg9rRU4mn5ajexpYLbO6SVMBJFn6yMPnuxP+npv9XR6Oexf1IXJq7ndftzNJD8lJ+eLN4tGnQyi82LheWVxa3UxN+/QqTSCykoeAZEqVU/vW9jG4OMD/A8AQVapZPXFvadqHlofodtvXIiw8pNo9iKFmtUk8TuFR5f/AKby9MHb+CBB2lNQMrABYE0KgkgMgQNB3by5Ifm74PoDHdQc8nr2e/PsjvPM4CBgAgnD1R3CR3f/+5wka5ppslUPoBgEJxDCQJwFDoIFwhsAwxiAAE0VnpRGKc8JoieMZA0MrYy0NTVNWe6wa1KnXOAqs47ZKPVasujW1dOXLrh8VHVNNM31hlVp0dVnrDCe6X0Oolp+rqQoHNO6f6u1jEy0DfVb1atQJDyDxv8QBAA==) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+0000-007F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+0080-1AFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+1B00-218F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+2190-21FF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAATcAAsAAAAABxwAAASRAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYdvGyAcKgZgADQBNgIkAxgEBgWBdAcgG1gGEVWcQwE+ElO4G3pA8hc0hxm3NGG6/8up/ZE0IXYD5JQJQigjvFYGThf5VABWk9PS4bI3/ONOAHFwCxNLNbXr539uoRZZW+ctNHr4gnzB5NNGgyUgiUYiIVpoeKSemIV0+UJPp2JbqHYhnb3wk1sQkAQghOQtP1J7bkNdbd2Ed62zY/jbl7IRwEZmARslsNEiK5EsWQLPzxKrPgHLtGmDHo/hs7NS+Xn+USVA+usP7yneyn36YVUB3suHHEIy8suHT5i5YPm6rYc++unqkcbGujbHjdmW+gr7CLfIUzhDQQrTuo/XttSemTplzszcNcs1Oz5nh0pMmTh5MRQqbf/oY/vHXP9DXwMOnc+b3Hz+2LnfThD+zwwgPdH0l+igeGDa0qWNYv8HACGOdMgUgIAA05z1MbwnI0g7uUL+FqYIF3LQo5Iv2a/iQ+zRyAbZYCgzxI1xQxXa+fHrj/kaHEPZFrVvewqux7GHn9KcT1rZn20Eb6kijsRPNDgW19O+3SrbkYpj+JrH1/lxtNO4wRg3lBsMsmyo5se5i7plv6viO3ZK0b+ej61sX8G2pFQyH3N9SwqPp/WReDhSFpGXpCqD4FiOzz1xI67ZBFu1PBKOk0k143po33Kdj+FrKNuxom93Kh+L6zcddktLJNPE62GS9LJRTphiemktn1+bjYTi/ymyysn1LXgjzqexaCLksj8sR8mGSRzehAQhUE4EcgaFUOeSv1QH/ccea54exiE4nPZ3pfKMbELUzfwI1WZhz8mhRCwqbcaDKQ0n2pIUZfIYgfhvojG9KWEoNxhlWV+d84KtI5jCNop4FodrmtCIKldv3eZv8C3qmoO9vOdzard57OV6vdtur3Y63VarxWs2lX3Je6kNT/iLbRLZj2y3xhvyBgNlfrffcbFKseu9et3r8p6m4FLUfcbf8Le39qCKahM39hoGxz+VrWyO5vOc8s1kLwlSCdcQ5qWoeyhVfB6lPBr1Kkr1pUtenwM7Q+5gVQP2Us9fKRWpxuw2WcssXttFR5Vdibqj2gTnP2ju5bpmfINv997mKlc/R+PSCM2R48nfE4sSvDWRwo/6UuU4+9ODOxNpvNYT8aSrGdcyVUVRkuuysq5FQxHF92NWdtI2uqgSgAY0CABkQIUqZPnK9dshDSQAgMFBAADACYw0mrBYzvkjzbVQwrRDrST8TBSgcpCYJbwDsHe15AuYOjthYdfuBVNNaip+PYBgECR9cW4PaCDMFbMa7CYVKrtIirIsEFmSQGuXbKRc08HBypbsnw0cCimurtT2sysP5c7/L02UfgEA+K3z8VP5dNPte4OjB/7f19JCEEAEQbNJfDyAvBmbGb7BLxA6PviHt3+eCEHIgTI7TbdTnrMaHXZaI9gMMgEgFfYSgUjpANAJzAoBCToVAQTQKyIAGEclnkKhjJGn6Dx9mTr1OjQ645TTmml9C1pTTTbF7Cs0oXVJ6yhXaG2uIJ+fN131vD7HnNPstBa1JtJa4jxjyFbehNATsv0Eba1Kx01c2g2ZnfCjmpxRp5bWFBNNTqsiwCCr53WbagEAAAA=) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+F8FF-10FFFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJwAAsAAAAAA/gAAAInAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYIOGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzMDIL4Mb4gcpY8lI6bRFpuvq4/OWEUk/8G00RY3bzZ45QTP/4/7fp977/hhqDRrEowk18nij7AjlZhdyoMv5jaxZE2Bj3goRCo0jjQxTYTacALb/XAD3u4CDzHDKgCagaf4WPsskuLqgQ3PZuX/n3qBs2kmff9/N/kG2b0PAne7CZpHlGAY9xJq09hCASUSoGTuSs0f2zl4eIgSJghGISKxbXaRXsuiPBy17rvw/Wa3uNuCe5R7qd2i2WdRPsGis68g+3ZpyKuftUT4Ml8LiPrqLUz7F5d4lxWHEbH1TwVznkJxQVFRUcPp5HpB22zRiZu6iSW3/FDmps8biI7jmrPj9tdyRFd98BQsNBZBEBFl2zjn+X2+8fxv6Xy4zzkaIU9y/93vGj4f6Sv2n94f9TP6mJ9Hjhzx9Fp4eubk6SNHH1qsnnViFoQdT0KAaDJlk7aOyw1hMAChEACF5cmnyrGLc12BiEREu1GP5QhhGG1RP8C4Wq3coCSGCSj319jF//XtoAt9W6KaQI4DVI22ALkMAAIIJP+TmbHrYDBPAH5p8KtXb689cwMKjQJAQH8K7tVQaL38SkDoDwAARJ3ABkOIABGM44iJAtYQEwTDGigUW6CBnayhJWGkZwXhRfafy2pu4jDqwF25ShQpVrUSDJE05ZqfwVAUZ+7L7O+kXdQXR7maswwqDVrA81Xk1w6+d1Rg+3GHm9q4LECxAkXRaxpCbqELSggA) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+0000-007F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+0080-1AFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+1B00-218F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAsQAAsAAAAAEVAAAArGAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADZp/GyAcKgZgAIEsATYCJANCBAYFgXQHIBuKEACO0xV3IEmSe0c05/9ekrsEyk8lPUoDXK5mnpohngqaBNGKEVwimNYU07a0Dv5E6RObSze/5SH/9u5+lMBf4FlUW4YJhFESSNAWUeABBtb1+7W6D/coHi000caFQieU++/MG3g+tYRpKCbNZPo/krili6RKyB4akRoZMjmgIZ2t37uCGbUBgiAREYR4s6Pn2T1enl5znmmFY0+5mZuCuKkWiJvG46bzubECPNqCz2jRpX7RV40mYMNCGIkLbREy2z3agpeE/JGt88H+N42B3tFIHARlLrGZOH3u4uVrdt5I1e477+ru+My6/v4uCtePCRYeFfr+js4qpetRX3P/L5Hg6XrU7ZyC9W1SGdedChfMnU+qAJCdmu58asZDffgbv+G77vrKzzmf/ZkEjL/HgMgu9Pv3czBkLqsdWFaMNkJEXEPIz75hYAOIQAUK8Ad1mHcgJQT+HmcDOSM2hNIRI5+4IURsIgLPQUqXPfWZWE5k8kieNy+d18X7i7+O3y8wEywS7Ba8I8eR9VSJ8Jpoueiu2SazAnMr82DzTyPUI+6INX1Giz7i/u8c9Tv/LmdN4znNc+qOMxPnQNjPlGLG73AcfN9/++n39/OxFz4+e5qbLHV+Vlj/tOVnmC2FdGwBk/BqKjbH8smlM2JI7Cd+4rz4XD3k0FqNPqmQMblFUGqN8qIXw7lRYkjrIoyj4QfaZFzOGSmxsdx/DGSADMtgoUQrqYEM7h3tonO/U12lv627rAtJZ9Ka9e/i0vfkWKnUB9iwnaLrVyIvXZKKjbrS/pdhIUdmzv4yl0BLtG0BWm3Dv2kd42MSohKSpNWqKkdXN6WTKkGVEcJIasL3KTdcCRWJ8UIo2yUMgP3YBmzOIwBb0g1epERryHzxTWF7fFJsSnS66FFoyRF7Xz/7wNiQxAhtqNrvsfVSmKjfWhvhI14IKdRVB37m3tKuwq9Sd8dNG+qW2uIXn747Qq0+OCEULssvSvEqy6xQIY5irIMutWYGQBtL4CNuBKYImNGhF+KCddp6t8TEKHWVg6u7R3qo494xwKssJdrsQ4rxYrjbnBeekHdu/X8UlAnq+nOxWEOuxqjFDwfGQPX3kipuNPeKlsjBaQp2wrKZIDP5/Mn5wJRh4aJtahmAvQPESMIHlvOhQYbXTDArHHv8CR7SX/EUWLOjehnVWFJi8ITjBKgWxLfsoneWKzray8vV3IryXbs8FLsYcQjYEu/A5olkP1HkY70hvZa2M/jdv2cwiGI/g/0BrsHgvZZ2+nj3+/oIpcHO3s+DM/N6cM/D3m52cVgD7HhGFL6GaV16z975XKVxC23ywcNvTnG+8Ik07fz/ZHqwvfOboT3Nixfv3SeP07pzSBbdRZ8KMDQ3ZusePMpRyvcH+J2WRXK7M+dvWrxonzxY39zkJw9QGkC96QM9VK3jIsptOwevUtzOORCwG6L53Bawojd3e95U3dfdK7r5qseu02rrnjXLDiyvX9q99OVe0Yr8fwTIa6dftnQP1g8e6F8zsLXVbofVq1VFLjp7lZ2ny+bVIvEAt5zI44DP9XNRtKkRf3myn2vkhKQ4pAf2dP3eBeoe4s0v/D6IpPGSkp+mAWMNlmD23Z+5MkxkkPKI7YfXWa861ASEUgbnqYQ+EodTkZmazCxpqiY1KoWBgATot1d/CmhtY3evdUfN8Z3Zsn9LyGHPyd2YsMbSKdgc7zwnO0pdXk4+oSLDNGFh0hBNSFQI40BdXkGK4SdunKaGAGnbv21g18Y3GLX0jv9Mo95xo37w/S2gTtiOZWQL1Qy2JK55zx1YLfRc7b7Z77TPA7dX57pF4pAa0D15AaDxHwPsc7Budm4E8ya3Fkl3BOcXQvc33a675V123CCT9KDyM6esUvrsZNWWTXaKQ27aM5VqRjIcccLPS+EiPdzo1M2YbMGB5so54mi1qRh/IRO57fTd+NLEnFQRzn0OsEsY4HtGfeSCiNtjek0/DSr28ZV6nws+ftw7syCvMP9xFiPpQfJfSOflZ1eUGYL8lAFKtyDGOVulz5X2V+N13T/PfqvT6mtdu15rdU6ftbl/rsmK2c8Jt3Nyeuh5VetNv1KPLJnEpC3Lyi4uk7Zsrd5y+kTguUCm2pU0ZOQnFlrfqrjkfsZLdVB2/mgxJcFsU4naWeF14dRpdUqJt8z+LOlQXK1qt16BL+8FCg7hGTS2GvDOLCP5XJMNu6CS1zeAwL8Y5Hhh3YOGtULzokDkBPLcwSEbxJ1LJ7ee7u6qfdctr9/irb4UESpL7CBhcQNWUhKj3B/LyTaq2JVkpualF1nfz/M9dPys6oTCKyXfS+bkSCru3Q28Z43H4sP0IyeUg5PzTFsORvNoWEhLwAGbkwzj5TMAK3FE45iPb+EALJkRfumh+8radc5I/lN8o3q54HqjJQ2nqGD1pYtB/vu37N0YOUn0looabuqq78zR5+oz9aIaXCWMNFXRjxy5co53tJrEa0zZB0nb/lP2VjfIjRzkwzTL76gakOo7sjpzFhatrJwt+o56BvInIAdX6UwKz1VjqQcex5z9Sfku+DfRLCpwus96j7miszBOBVKYK51FYdcHWO5MwkC5cTx9VozPmvvJ8HFLjvmFy8QazqSB5WPuD3KTBiVVMM7ye6p6+D6MMPyum3YPj6icKfqeknjXwIG7nAScOzPfgmDc7I5nMEdghup96Fu9h5D1fusO42kiBUwPhWZYIJ1BSaqw8x2TZIQDnQV8nn/ZkMqxL7d3/NffGsfREA+TjwzhcLyKxNzue+PPrX45YP7VxsIwPLmi7fsD66D2cyB/5H5Y/5NLP3T+QMD+T3w4AK30xW3KjYdXihZzJ/rw38LHH/Qfkt6IYDsV30/iHVS0JjpamqZJi0xjYHO8314qRjNGGhmviY3z3UOqJilB+mP5neYHT73tSpiWJrLdY8fNpdZ41JbVeEKgJiAqUDaVurKcBD4VGhUSLJ0QgeGvdTqe8ZPjMJQNjwGzYUka2MIyGi8bFgnhqlBy7EcctvwpVNtK+4SroRSGh017Etdm0u2fJxSHZBh7M9dlYnUmhZ3ihVFr4fo1cCRThD01qRqzLvP6EV1x5EZvGSdZWNSnxibGxSflWfzPOH2sLULkQSMCQuZsMIh56+5DSETwceyOiJEnaqrpeFMTL45KT2SVE6FCQT6mYkT6UxQL3kuE0ImJiQstjCOI5BABsSRIWyMm2PELv4BMxL8Js/UIJUYg3sC34LFtcngG8TljfTzC+gukFTFGxR5MXK+wY1kpy7AT2ansfHYlu5nNY8smTJwwdYJdby2ZlZWwVqwNS3AetMLZ4HDyuQ78PjA88OvALwM/DvQPvBvoRGjArf/P/p7+B4jA4FmsWNuyYU1qfEqEDIMgdpuy/DIT7Ueu/lfEF1QhhFBPUGOzf+ULT2q/fv3yySxG8AER+Hj7twX/+QIoqpXncPtqIkwNZH1EJiG5WU6zCgr9gb5DFXQaRa1vCO1i7b5v0XiWrEw12SRfxTKzzHEO2gfJIRZCQifwEY0ZQigICscQBIJieHjCYvgQimgqyFdBkqbxqDKH38SLt0C+FNx5UGIVYy003wLLO0kIu1KWk0CrK+dB4ZwguLYJOTtLGS9V8TTXwgaco2qtPL8YjqsfuXLUpsjFXIrCJUfEE6mJFbx4Yi3gzRcBfKgAX7PQQggA) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+2190-21FF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAUwAAsAAAAAB1AAAATmAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYgiGyAcKgZgADQBNgIkAxgEBgWBdAcgG4sGAI7TpTZgEnN0/bdZ/WbmEV2iJA6zYsqmkhMnIk5BTAcihkUMkpy4sKwZOZW/86+rntyLICrXkQ0QR08+dWrm0gQ092BRC9DqXtSrFmhMLHCpwL2epzzWACpGhKqRXuEpp/dUeAUCAgeEEGVSsaH2AaPBuPtMK1N7Z4K8GeQtgSBv5eRtvBwusJBAXv0Y9B08uGa1Cq8P9LGwZ+MB/BzxAMI3FN8NuvJ/Wyh+GAJKQvxDYjftPnI66b6Uwkdeeru4ocHYGjOaaq4brORYs5kiqzHz43HWWGZoNlQf2H/8SMsd5Y5Fjq9N5ffv2QcdWSrnbSvN237wz3wHOHNnXr9OX1r74QSeH2ZgOI82/g8dsxCCYs+IPs2/AICQIT8I4IAAB1odPoJ/yf3kNfI7t4Mr5p5UvokvJnrtWBYaZvbgd6pOqUvqipGme1xmdViRfcQxYrfgMKuIct9ip1BD5VM/LmsVqEUx6zZVWvExKy6G4kfesCLcgU4ViqilYY8tJ/4on/VBDTt1y82q0EpdZpOrO7ZL6uqUNKyMWanyTVnL6ry441JomBkvyeUqPMNEuqw9Lp9SsJNM474Lh7CChhXZbQ673e7rmpmedsbMdTk7nWpmwTKqHG+Rf2sl+M6PPL69oOp0OjvnYl2zrrkZDVah5S4307BTVE44vpzkw0TU3plFld9ji85rxnvIW/ilSjJJZilGmu6d7VE7hh0jjhF8gJ2Jct9k3L8UryoYt9V9Nz6AZxx2dKEjenZ6esoZ4zTNdM+qWTomZicht5Wy6wrk/s1epEpWu7RgRRPBj7z8P7JbtZiFgRhFV+72YYEYnZSJmayKSrMzkjPWOTU9O62xOUYddgemYHHUXW4WwKLp0t0+GMCibrlZChYl61cc0c6e3ikpVjKbuk0a5UU5nSAvZ/LYhBtUTWhEDzt34SL7DD9DfcvMr+zLJ+nggG0wtnKwv1JjG7IODsY8w76ing/9W87i5ffkDJXDxzZlm5iMmbJM9Y+pbRY60S/ZuzrK4sC+bJw5jPrH2Gc444IOPXiuiRl/lajyciAfvVs+o3qCGZkHPck6/Iy5mf5x8yH8somOjVvGYp9acDw7YZmxTGnq8UuvdqJHCOivGrIOWgbVnPNwTI9tYGxA3T/hGnH5pWwuxC+fMP/G9M34GbqTLzIP8zyBxiMuquyaW/phXjvPWuYVrGTcp2tO/smKGfO+zGCdtfp5/T0B3rGxsTeXNgYGemZHJ8fGp54KXLe0LTwegE6qgACAP8RRI0l33p8GviAAAKytAXjcqFtsww2m1Bc3GSDKJQ15gfsI40B5DR/I3QaAnOGSp+BAMmChxbPhsfVj6138s6sI+GeJ31mASQAA7gQfCABinAFuoPlAOBcivCyASAsUKa3/tbWaWuqnejdBqM1hrcZr7S0MOvWfLy+8BgDwfceFS/zsno/da2urf/s5hG+AAA+czwr+wiqy+etcSGvLPYAAfJ9wU6ATLdkByYKglFGJEkagAwEA4AM5eCCCHwB0ABkQEKADcMCBBHgAMHcL7ooUYmwhhZ/zExnVadegWqUqTUTPg+iAffY71jDJpONEJdqJdKpU09er6i1DqVpNqjQz2EOUQO+iM9RQoyZQztPlgZahUGYP0qZkBv24RtWMDET77bGvVngAHWsLcAAAAAA=) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+F8FF-10FFFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJ0AAsAAAAAA/QAAAIqAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYILGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzADAC4D7IZjzymg62laLb5im9YqKYd4pp0IH5Q4wfM11t7f3RNPmCSGpIkh4c2yaCYkPItas2reuIZYSNQTM2Ss3ZtaSFrNsPSi0pFIpbFOplQSozQTgtx2D1DYR6JuhAypGLvREA2A7uApPgY1OYgeC8Ayvq0tfgCOy3/OjT9vz2+AA909pwF9/n14lic0TDDgLsCov3cD7HJP9eTYIyKzTeFwCA8+E4xCRLzVnb12vN/reyL9TgUEkfJgglcieJcPbf0Qt29BkrLv8J7zzxfL3bbhyD1wjGfBvFlGfC+BrvfzhBHx1jVXVjwp4PWnrNv+wl6+cBzQyXu6yQ23capTbj9uoPHOagFvx3+WIwca9UT0znIXQESUt2/589nB/3tmaqS0NLrz8cJ7wfRIj9sRh9955/JPstfOQqZk3y3czExOze66PSH71ADLTBIBnGTKJtW1sRzoGIAEAGBYnnxqGre7c9KDHAkko875Ih2j3eoVNMnzyi47CgTI/R/Y5O+BdBB/hz4QRzmYAVQZiQ3kMgAIIPD/xF1r5/lKD3MM+Fw1wOfx8Ya/07h5I9AoAAT0Q/wvR2PC/4+GwJWJmuVBIwSw0URCjAPogY6CoQcVigHUwNDQ4DoL6WWVtc7zaH+wHDXrjYnekaVEkWKlS6TJfCtX11KvsdHs+BudH+X2pDHteZVAhURkrVZjS6jKUF2dVSteBQb3BfvWuNnv6RUrUMSpBo7ENgAAAA==) format('woff2'); +} diff --git a/docs/md _sync/assets/highlight.css b/docs/md _sync/assets/highlight.css new file mode 100644 index 00000000..e69de29b diff --git a/docs/md _sync/assets/highlight.min.js b/docs/md _sync/assets/highlight.min.js new file mode 100644 index 00000000..f43ba9aa --- /dev/null +++ b/docs/md _sync/assets/highlight.min.js @@ -0,0 +1,1213 @@ +/*! + Highlight.js v11.9.0 (git: f47103d4f1) + (c) 2006-2023 undefined and other contributors + License: BSD-3-Clause + */ + var hljs=function(){"use strict";function e(n){ + return n instanceof Map?n.clear=n.delete=n.set=()=>{ + throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{ + throw Error("set is read-only") + }),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{ + const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a) + })),n}class n{constructor(e){ + void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} + ignoreMatch(){this.isMatchIgnored=!0}}function t(e){ + return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") + }function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n] + ;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope + ;class r{constructor(e,n){ + this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){ + this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{ + if(e.startsWith("language:"))return e.replace("language:","language-") + ;if(e.includes(".")){const t=e.split(".") + ;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ") + }return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)} + closeNode(e){i(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ + this.buffer+=``}}const s=(e={})=>{const n={children:[]} + ;return Object.assign(n,e),n};class o{constructor(){ + this.rootNode=s(),this.stack=[this.rootNode]}get top(){ + return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ + this.top.children.push(e)}openNode(e){const n=s({scope:e}) + ;this.add(n),this.stack.push(n)}closeNode(){ + if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ + for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} + walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){ + return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n), + n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){ + "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ + o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e} + addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ + this.closeNode()}__addSublanguage(e,n){const t=e.root + ;n&&(t.scope="language:"+n),this.add(t)}toHTML(){ + return new r(this,this.options).value()}finalize(){ + return this.closeAllNodes(),!0}}function c(e){ + return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")} + function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")} + function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{ + const n=e[e.length-1] + ;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} + })(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"} + function p(e){return RegExp(e.toString()+"|").exec("").length-1} + const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ + ;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t + ;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break} + i+=a.substring(0,e.index), + a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0], + "("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)} + const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={ + begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", + illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", + contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n, + contains:[]},t);i.contains.push({scope:"doctag", + begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", + end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) + ;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) + ;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i + },M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({ + __proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{ + scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x, + C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number", + begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{ + "on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{ + n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f, + MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0}, + NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, + PHRASAL_WORDS_MODE:{ + begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ + },QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, + end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}, + RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", + SHEBANG:(e={})=>{const n=/^#![ ]*\// + ;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n, + end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)}, + TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E, + UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){ + "."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){ + void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){ + n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", + e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, + void 0===e.relevance&&(e.relevance=0))}function I(e,n){ + Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){ + if(e.match){ + if(e.begin||e.end)throw Error("begin & end are not supported with match") + ;e.begin=e.match,delete e.match}}function B(e,n){ + void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return + ;if(e.starts)throw Error("beforeMatch cannot be used with starts") + ;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n] + })),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={ + relevance:0,contains:[Object.assign(t,{endsParent:!0})] + },e.relevance=0,delete t.beforeMatch + },z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword" + ;function U(e,n,t=F){const a=Object.create(null) + ;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{ + Object.assign(a,U(e[t],n,t))})),a;function i(e,t){ + n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|") + ;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){ + return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{ + console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{ + P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0) + },G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={} + ;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1]) + ;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{ + e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, + delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ + _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope + }),(e=>{if(Array.isArray(e.begin)){ + if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), + G + ;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), + G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{ + if(Array.isArray(e.end)){ + if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), + G + ;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), + G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){ + function n(n,t){ + return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":"")) + }class t{constructor(){ + this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} + addRule(e,n){ + n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]), + this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) + ;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|" + }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex + ;const n=this.matcherRe.exec(e);if(!n)return null + ;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t] + ;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){ + this.rules=[],this.multiRegexes=[], + this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ + if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t + ;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))), + n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){ + return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){ + this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){ + const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex + ;let t=n.exec(e) + ;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{ + const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)} + return t&&(this.regexIndex+=t.position+1, + this.regexIndex===this.count&&this.considerAll()),t}} + if(e.compilerExtensions||(e.compilerExtensions=[]), + e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") + ;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r + ;if(r.isCompiled)return o + ;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))), + r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null + ;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), + l=r.keywords.$pattern, + delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)), + o.keywordPatternRe=n(l,!0), + s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), + r.end&&(o.endRe=n(o.end)), + o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)), + r.illegal&&(o.illegalRe=n(r.illegal)), + r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{ + variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{ + starts:e.starts?a(e.starts):null + }):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o) + })),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i + ;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin" + }))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end" + }),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){ + return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{ + constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}} + const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{ + const a=Object.create(null),i=Object.create(null),r=[];let s=!0 + ;const o="Could not find the language '{}', did you forget to load/include a language module?",c={ + disableAutodetect:!0,name:"Plain text",contains:[]};let p={ + ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, + languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", + cssSelector:"pre code",languages:null,__emitter:l};function _(e){ + return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i="" + ;"object"==typeof n?(a=e, + t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."), + q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), + i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r) + ;const s=r.result?r.result:f(r.language,r.code,t) + ;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){ + const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A) + ;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t="" + ;for(;n;){t+=A.substring(e,n.index) + ;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){ + const[e,a]=r + ;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{ + const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0] + ;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a + ;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{ + if(""===A)return;let e=null;if("string"==typeof x.subLanguage){ + if(!a[x.subLanguage])return void S.addText(A) + ;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top + }else e=E(A,x.subLanguage.length?x.subLanguage:null) + ;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language) + })():c(),A=""}function g(e,n){ + ""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1 + ;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue} + const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}} + function b(e,n){ + return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope), + e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), + A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{ + value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n) + ;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e) + ;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){ + for(;e.endsParent&&e.parent;)e=e.parent;return e}} + if(e.endsWithParent)return m(e.parent,t,a)}function _(e){ + return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){ + const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x + ;x.endScope&&x.endScope._wrap?(d(), + g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(), + u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n), + d(),r.excludeEnd&&(A=n));do{ + x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent + }while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length} + let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0 + ;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){ + if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`) + ;throw n.languageName=e,n.badRule=y.rule,n}return 1} + if(y=r,"begin"===r.type)return(e=>{ + const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]] + ;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t) + ;return a.skip?A+=t:(a.excludeBegin&&(A+=t), + d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r) + ;if("illegal"===r.type&&!i){ + const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"') + ;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e} + if("illegal"===r.type&&""===o)return 1 + ;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches") + ;return A+=o,o.length}const w=v(e) + ;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"') + ;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[] + ;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope) + ;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{ + if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){ + R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T + ;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e) + ;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e, + value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){ + if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t), + illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T, + context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{ + language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x} + ;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{ + const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)} + ;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1))) + ;i.unshift(t);const r=i.sort(((e,n)=>{ + if(e.relevance!==n.relevance)return n.relevance-e.relevance + ;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1 + ;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s + ;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{ + let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"" + ;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1]) + ;return n||(H(o.replace("{}",t[1])), + H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"} + return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return + ;if(x("before:highlightElement",{el:e,language:t + }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) + ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), + console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), + console.warn("The element with unescaped HTML:"), + console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML) + ;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a) + ;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t + ;e.classList.add("hljs"),e.classList.add("language-"+a) + })(e,t,r.language),e.result={language:r.language,re:r.relevance, + relevance:r.relevance},r.secondBest&&(e.secondBest={ + language:r.secondBest.language,relevance:r.secondBest.relevance + }),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){ + "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0 + }function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]} + function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ + i[e.toLowerCase()]=n}))}function k(e){const n=v(e) + ;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{ + e[t]&&e[t](n)}))} + "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ + N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w, + highlightElement:y, + highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"), + q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, + initHighlighting:()=>{ + w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, + initHighlightingOnLoad:()=>{ + w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") + },registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){ + if(K("Language definition for '{}' could not be registered.".replace("{}",e)), + !s)throw n;K(n),i=c} + i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{ + languageName:e})},unregisterLanguage:e=>{delete a[e] + ;for(const n of Object.keys(i))i[n]===e&&delete i[n]}, + listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O, + autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ + e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{ + e["before:highlightBlock"](Object.assign({block:n.el},n)) + }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{ + e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}, + removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{ + s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b, + lookahead:d,either:m,optional:u,anyNumberOfTimes:g} + ;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t + },te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{ + scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{ + scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/}, + FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/}, + ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", + contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ + scope:"number", + begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", + relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} + }),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le) + ;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={ + className:"number",variants:[{ + begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{ + begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ + begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{ + begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{ + begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{ + begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], + relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))} + const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye) + ;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/, + end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ + const t=e[0].length+e.index,a=e.input[t] + ;if("<"===a||","===a)return void n.ignoreMatch();let i + ;">"===a&&(((e,{after:n})=>{const t="",M={ + match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)], + keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]} + ;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{ + PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/, + contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ + label:"use_strict",className:"meta",relevance:10, + begin:/^\s*['"]use (strict|asm)['"]/ + },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{ + className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{ + begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", + keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{ + className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{ + className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{ + className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, + excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/, + relevance:0},{variants:[{begin:"<>",end:""},{ + match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin, + "on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{ + begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{ + beginKeywords:"while if switch catch for"},{ + begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", + returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t, + className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t, + relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, + contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, + className:"variable.constant"},E,k,{match:/\$[(.]/}]}} + const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] + ;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={ + begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]} + ;Object.assign(t,{className:"variable",variants:[{ + begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={ + className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={ + begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, + end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/, + contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/, + end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] + },l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 + }),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, + contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ + name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, + keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], + literal:["true","false"], + built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] + },contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{ + match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}, + grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}] + }),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ + className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ + match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{ + begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ + begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", + end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ + begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ + className:"number",variants:[{begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" + },{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ + keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" + },contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ + className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ + className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 + },g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ + keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], + type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], + literal:"true false NULL", + built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" + },b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{ + begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], + keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u, + contains:b.concat(["self"]),relevance:0}]),relevance:0},p={ + begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, + keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ + begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], + relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, + keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/, + end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s] + }]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, + disableAutodetect:!0,illegal:"=]/,contains:[{ + beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, + strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{ + contains:[{begin:/\\\n/}] + }),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ + className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{ + begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ + begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", + end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ + begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ + className:"number",variants:[{begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" + },{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ + keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" + },contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ + className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ + className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 + },g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ + type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], + keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], + literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], + _type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] + },b={className:"function.dispatch",relevance:0,keywords:{ + _hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] + }, + begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/)) + },m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{ + begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], + keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, + contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function", + begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, + keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ + begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{ + begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{ + className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0, + contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u, + relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}] + },s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++", + aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{ + match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], + className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={ + keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), + built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], + literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{ + begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{ + begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] + },r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/, + keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/, + end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ + },e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{ + begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/, + contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]}) + ;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE], + o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{ + illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] + },u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t] + },b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={ + begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], + keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, + contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ + begin:"\x3c!--|--\x3e"},{begin:""}]}] + }),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", + end:"$",keywords:{ + keyword:"if else elif endif define undef warning error line region endregion pragma checksum" + }},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, + illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" + },t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", + relevance:0,end:/[{;=]/,illegal:/[^\s:]/, + contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ + beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, + contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", + begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ + className:"string",begin:/"/,end:/"/}]},{ + beginKeywords:"new return throw await else",relevance:0},{className:"function", + begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, + end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ + beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", + relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, + contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params", + begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, + contains:[g,a,e.C_BLOCK_COMMENT_MODE] + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{ + const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{ + name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{ + keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"}, + contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ + },t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 + },{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 + },t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ + begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}] + },t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{ + begin:/:/,end:/[;}{]/, + contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{ + begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" + },contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0, + excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]", + relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ + },{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ + $pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{ + begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{ + className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{ + const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{ + className:"meta",relevance:10, + match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) + },{className:"comment",variants:[{ + begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), + end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ + className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, + end:/$/}]}},grmr_go:e=>{const n={ + keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], + type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], + literal:["true","false","iota","nil"], + built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] + };return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex;return{name:"GraphQL",aliases:["gql"], + case_insensitive:!0,disableAutodetect:!1,keywords:{ + keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], + literal:["true","false","null"]}, + contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ + scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", + begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, + end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ + scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)), + relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={ + className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{ + begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/, + end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{ + begin:/\$\{(.*?)\}/}]},r={className:"literal", + begin:/\bon|off|true|false|yes|no\b/},s={className:"string", + contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{ + begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}] + },o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0 + },l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ + name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, + contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{ + begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)), + className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{ + const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={ + keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], + literal:["false","true","null"], + type:["char","boolean","long","float","int","byte","short","double"], + built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{ + begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/, + end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} + ;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/, + contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, + relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ + begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, + className:"string",contains:[e.BACKSLASH_ESCAPE] + },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ + match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ + 1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ + begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type", + 3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", + 3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ + beginKeywords:"new throw return else",relevance:0},{ + begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ + 2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/, + end:/\)/,keywords:i,relevance:0, + contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE] + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe, + grmr_json:e=>{const n=["true","false","null"],t={scope:"literal", + beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{ + className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ + match:/[{}[\],:]/,className:"punctuation",relevance:0 + },e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], + illegal:"\\S"}},grmr_kotlin:e=>{const n={ + keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", + built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", + literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" + },a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={ + className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", + variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'", + illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, + contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={ + className:"meta", + begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" + },o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, + end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] + },l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={ + variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, + contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g], + {name:"Kotlin",aliases:["kt","kts"],keywords:n, + contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", + begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword", + begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", + begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$", + returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ + begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, + contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, + keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, + endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, + endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0 + },e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{ + begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ + 3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, + illegal:"extends implements",contains:[{ + beginKeywords:"public protected internal private constructor" + },e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, + excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, + excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env", + end:"$",illegal:"\n"},l]}},grmr_less:e=>{ + const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({ + className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n, + relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only", + attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c, + relevance:0} + ;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{ + begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", + excludeEnd:!0} + },n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{ + className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 + },n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({ + begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0, + contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:", + returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ + },n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b", + end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}] + },m={className:"keyword", + begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", + starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={ + className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a + }],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{ + begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0, + returnEnd:!0,illegal:"[<='$\"]",relevance:0, + contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{ + begin:"\\b("+re.join("|")+")\\b",className:"selector-tag" + },n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{ + className:"selector-pseudo",begin:":("+oe.join("|")+")"},{ + className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/, + end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={ + begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]} + ;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH), + {name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}}, + grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"] + },i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10 + })];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, + literal:"true false nil", + keyword:"and break do else elseif end for goto if in local not or repeat return then until while", + built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" + },contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)", + contains:[e.inherit(e.TITLE_MODE,{ + begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", + begin:"\\(",endsWithParent:!0,contains:i}].concat(i) + },e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", + begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={ + className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", + contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{ + const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={ + variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{ + begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, + relevance:2},{ + begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), + relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ + begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ + },{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, + returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", + excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", + end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[], + variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] + },i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ + begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[] + }),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r) + ;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o) + })),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ + className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{ + begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", + contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", + end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o, + end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ + begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ + begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", + contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ + begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ + className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ + className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{ + const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n, + keyword:["@interface","@class","@protocol","@implementation"]};return{ + name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"], + keywords:{"variable.language":["this","super"],$pattern:n, + keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], + literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], + built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], + type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] + },illegal:"/,end:/$/,illegal:"\\n" + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", + begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t, + contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, + relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={ + $pattern:/[\w.]+/, + keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" + },i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/, + end:/\}/},s={variants:[{begin:/\$\d/},{ + begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") + },{begin:/[$%@][^\s\w{]/,relevance:0}] + },o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{ + const r="\\1"===i?i:n.concat(i,a) + ;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t) + },d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ + endsWithParent:!0}),r,{className:"string",contains:o,variants:[{ + begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", + end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ + begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", + relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", + contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", + contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ + begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number", + begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", + relevance:0},{ + begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", + keywords:"split return print reverse grep",relevance:0, + contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ + begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{ + begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{ + className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ + begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0 + }),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{ + begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub", + end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{ + begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$", + subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}] + }];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a, + contains:g}},grmr_php:e=>{ + const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={ + scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{ + begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null + }),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ + illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{ + begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, + contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{ + n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{ + n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({ + begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ + begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ + begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ + begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" + }],relevance:0 + },g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={ + keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{ + n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase()) + })),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{ + match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{ + 1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{ + match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" + }},{match:[/::/,/class/],scope:{2:"variable.language"}},{ + match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", + 3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))], + scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class", + 3:"variable.language"}}]},E={scope:"attr", + match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0, + begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_] + },N={relevance:0, + match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)], + scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N) + ;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1, + keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/, + endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{ + begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]}, + contains:["self",...w]},...w,{scope:"meta",match:i}] + },e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ + scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, + keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, + contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ + begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ + begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{ + match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{ + scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, + excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" + },e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", + begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m, + contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{ + beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", + illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ + beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ + beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, + contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ + beginKeywords:"use",relevance:0,end:";",contains:[{ + match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]} + },grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{ + begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", + end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0 + },e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null, + skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null, + contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text", + aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{ + const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ + $pattern:/[A-Za-z]\w+|__\w+__/,keyword:a, + built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], + literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], + type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] + },r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/, + end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={ + className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ + begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, + contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ + begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, + contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ + begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, + contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, + end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/, + relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ + begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, + end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, + contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, + contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] + },c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={ + className:"number",relevance:0,variants:[{ + begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{ + begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{ + begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})` + },{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})` + }]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, + contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ + className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, + end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, + contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{ + name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, + illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if", + relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{ + 1:"keyword",3:"title.function"},contains:[m]},{variants:[{ + match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}], + scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ + className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}}, + grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt", + starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{ + begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{ + const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) + ;return{name:"R",keywords:{$pattern:t, + keyword:"function if in break next repeat else for while", + literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", + built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" + },contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, + starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), + endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ + scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 + }]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] + }),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], + variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', + relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ + 1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"}, + match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{ + 2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"}, + match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{ + match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`", + contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{ + const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={ + "variable.constant":["__FILE__","__LINE__","__ENCODING__"], + "variable.language":["self","super"], + keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], + built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], + literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={ + begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s] + }),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10 + }),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/, + end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c], + variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ + begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ + begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, + end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ + begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ + begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ + begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ + begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ + begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), + contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, + contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number", + relevance:0,variants:[{ + begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{ + begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" + },{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ + begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ + begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{ + className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, + keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ + match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", + 4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{ + 2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{ + 1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, + className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{ + match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{ + begin:e.IDENT_RE+"::"},{className:"symbol", + begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", + begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable", + begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ + className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, + relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", + keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c], + illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ + begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", + end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l) + ;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m} + },{className:"meta.prompt", + begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", + starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby", + aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/, + contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}}, + grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0, + begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/)) + },a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] + ;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r, + keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], + literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}}, + grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable", + begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", + case_insensitive:!0,illegal:"[=/|']", + contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{ + className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ + className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 + },n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", + begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo", + begin:":("+a.join("|")+")"},{className:"selector-pseudo", + begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/, + contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute", + begin:"\\b("+ce.join("|")+")\\b"},{ + begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" + },{begin:/:/,end:/[;}{]/,relevance:0, + contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH] + },{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{ + begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, + keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i, + className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" + },r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE] + },n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session", + aliases:["console","shellsession"],contains:[{className:"meta.prompt", + begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, + subLanguage:"bash"}}]}),grmr_sql:e=>{ + const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={ + begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} + ;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ + $pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t + ;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)) + })(l,{when:e=>e.length<3}),literal:a,type:i, + built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] + },contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, + keyword:l.concat(s),literal:a,type:i}},{className:"type", + begin:n.either("double precision","large object","with timezone","without timezone") + },c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", + variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, + contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ + className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, + relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0 + },t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={ + match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)), + relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{ + className:"keyword", + match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={ + $pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{ + match:b(/\./,m(...De)),relevance:0},{className:"built_in", + match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{ + className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}] + }],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0, + variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{ + match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/ + },{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{ + match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] + }),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) + }),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/ + }),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)] + }),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={ + className:"string", + variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")] + },k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, + contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, + contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t, + contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={ + scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/) + },C=[A,{className:"variable",match:/\$\d+/},{className:"variable", + match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ + contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{ + scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={ + match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type", + match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+") + },{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{ + match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={ + begin://,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D) + ;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{ + match:b(Fe,/\s*:/),keywords:"_|0",relevance:0 + },...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin://, + keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l, + contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/, + relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params", + match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={ + match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword", + 3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={ + match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, + contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{ + 1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{ + 1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/} + ;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label)) + ;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/, + end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l, + contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor", + end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{ + className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c] + },F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0 + },S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{ + const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={ + beginKeywords:"namespace",end:/\{/,excludeEnd:!0, + contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/, + excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a}, + contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e, + keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), + literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta", + begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n)) + ;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)} + ;return Object.assign(n.keywords,s), + n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]), + l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10, + begin:/^\s*['"]use strict['"]/ + }),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{ + name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{ + const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={ + className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ + begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ + begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}] + },o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] + }),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) + ;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, + classNameAliases:{label:"symbol"},keywords:{ + keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", + built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", + type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", + literal:"true false nothing"}, + illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ + className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, + end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0, + variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ + },{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ + begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ + className:"label",begin:/^\w+:/},o,l,{className:"meta", + begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, + end:/$/,keywords:{ + keyword:"const disable else elseif enable end externalsource if region then"}, + contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/) + ;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, + keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] + },contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/], + className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ + match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ + begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", + 3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, + className:"type"},{className:"keyword", + match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ + },{className:"number",relevance:0, + match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ + }]}},grmr_xml:e=>{ + const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={ + className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/, + contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] + },r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{ + className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={ + endsWithParent:!0,illegal:/`]+/}]}]}]};return{ + name:"HTML, XML", + aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], + case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{ + className:"meta",begin://,contains:[i,r,o,s]}]}] + },e.COMMENT(//,{relevance:10}),{begin://, + relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, + relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", + begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{ + end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", + begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{ + end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ + className:"tag",begin:/<>|<\/>/},{className:"tag", + begin:n.concat(//,/>/,/\s/)))), + end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{ + className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{ + className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} + },grmr_yaml:e=>{ + const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={ + className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ + },{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", + variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{ + variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={ + end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/, + end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]", + contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{ + begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ + begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", + relevance:10},{className:"string", + begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ + begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, + relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type", + begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t + },{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", + begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", + relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ + className:"number", + begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" + },{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l] + ;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0, + aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){ + const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])} + return He}() + ;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); \ No newline at end of file diff --git a/docs/md _sync/assets/highlight_init.js b/docs/md _sync/assets/highlight_init.js new file mode 100644 index 00000000..e2379278 --- /dev/null +++ b/docs/md _sync/assets/highlight_init.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', (event) => { + document.querySelectorAll('pre code').forEach((block) => { + hljs.highlightBlock(block); + }); + }); + \ No newline at end of file diff --git a/docs/md _sync/assets/styles.css b/docs/md _sync/assets/styles.css new file mode 100644 index 00000000..f103474f --- /dev/null +++ b/docs/md _sync/assets/styles.css @@ -0,0 +1,153 @@ +@font-face { + font-family: "Monaco"; + font-style: normal; + font-weight: normal; + src: local("Monaco"), url("Monaco.woff") format("woff"); +} + +:root { + --global-font-size: 16px; + --global-line-height: 1.5em; + --global-space: 10px; + --font-stack: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, + Courier New, monospace, serif; + --font-stack: dm, Monaco, Courier New, monospace, serif; + --mono-font-stack: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, + Courier New, monospace, serif; + + --background-color: #151515; /* Dark background */ + --font-color: #eaeaea; /* Light font color for contrast */ + --invert-font-color: #151515; /* Dark color for inverted elements */ + --primary-color: #1a95e0; /* Primary color can remain the same or be adjusted for better contrast */ + --secondary-color: #727578; /* Secondary color for less important text */ + --error-color: #ff5555; /* Bright color for errors */ + --progress-bar-background: #444; /* Darker background for progress bar */ + --progress-bar-fill: #1a95e0; /* Bright color for progress bar fill */ + --code-bg-color: #1e1e1e; /* Darker background for code blocks */ + --input-style: solid; /* Keeping input style solid */ + --block-background-color: #202020; /* Darker background for block elements */ + --global-font-color: #eaeaea; /* Light font color for global elements */ + + --background-color: #222225; + + --background-color: #070708; + --page-width: 70em; + --font-color: #e8e9ed; + --invert-font-color: #222225; + --secondary-color: #a3abba; + --secondary-color: #d5cec0; + --tertiary-color: #a3abba; + --primary-color: #09b5a5; /* Updated to the brand color */ + --primary-color: #50ffff; /* Updated to the brand color */ + --error-color: #ff3c74; + --progress-bar-background: #3f3f44; + --progress-bar-fill: #09b5a5; /* Updated to the brand color */ + --code-bg-color: #3f3f44; + --input-style: solid; + --display-h1-decoration: none; + + --display-h1-decoration: none; +} + +/* body { + background-color: var(--background-color); + color: var(--font-color); +} + +a { + color: var(--primary-color); +} + +a:hover { + background-color: var(--primary-color); + color: var(--invert-font-color); +} + +blockquote::after { + color: #444; +} + +pre, code { + background-color: var(--code-bg-color); + color: var(--font-color); +} + +.terminal-nav:first-child { + border-bottom: 1px dashed var(--secondary-color); +} */ + +.terminal-mkdocs-main-content { + line-height: var(--global-line-height); +} + +strong, +.highlight { + /* background: url(//s2.svgbox.net/pen-brushes.svg?ic=brush-1&color=50ffff); */ + background-color: #50ffff33; +} + +.terminal-card > header { + color: var(--font-color); + text-align: center; + background-color: var(--progress-bar-background); + padding: 0.3em 0.5em; +} +.btn.btn-sm { + color: var(--font-color); + padding: 0.2em 0.5em; + font-size: 0.8em; +} + +.loading-message { + display: none; + margin-top: 20px; +} + +.response-section { + display: none; + padding-top: 20px; +} + +.tabs { + display: flex; + flex-direction: column; +} +.tab-list { + display: flex; + padding: 0; + margin: 0; + list-style-type: none; + border-bottom: 1px solid var(--font-color); +} +.tab-item { + cursor: pointer; + padding: 10px; + border: 1px solid var(--font-color); + margin-right: -1px; + border-bottom: none; +} +.tab-item:hover, +.tab-item:focus, +.tab-item:active { + background-color: var(--progress-bar-background); +} +.tab-content { + display: none; + border: 1px solid var(--font-color); + border-top: none; +} +.tab-content:first-of-type { + display: block; +} + +.tab-content header { + padding: 0.5em; + display: flex; + justify-content: end; + align-items: center; + background-color: var(--progress-bar-background); +} +.tab-content pre { + margin: 0; + max-height: 300px; overflow: auto; border:none; +} \ No newline at end of file diff --git a/docs/md _sync/changelog.md b/docs/md _sync/changelog.md new file mode 100644 index 00000000..03e490f6 --- /dev/null +++ b/docs/md _sync/changelog.md @@ -0,0 +1,102 @@ +# Changelog + +## [v0.2.77] - 2024-08-04 + +Significant improvements in text processing and performance: + +- 🚀 **Dependency reduction**: Removed dependency on spaCy model for text chunk labeling in cosine extraction strategy. +- 🤖 **Transformer upgrade**: Implemented text sequence classification using a transformer model for labeling text chunks. +- ⚡ **Performance enhancement**: Improved model loading speed due to removal of spaCy dependency. +- 🔧 **Future-proofing**: Laid groundwork for potential complete removal of spaCy dependency in future versions. + +These changes address issue #68 and provide a foundation for faster, more efficient text processing in Crawl4AI. + +## [v0.2.76] - 2024-08-02 + +Major improvements in functionality, performance, and cross-platform compatibility! 🚀 + +- 🐳 **Docker enhancements**: Significantly improved Dockerfile for easy installation on Linux, Mac, and Windows. +- 🌐 **Official Docker Hub image**: Launched our first official image on Docker Hub for streamlined deployment. +- 🔧 **Selenium upgrade**: Removed dependency on ChromeDriver, now using Selenium's built-in capabilities for better compatibility. +- 🖼️ **Image description**: Implemented ability to generate textual descriptions for extracted images from web pages. +- ⚡ **Performance boost**: Various improvements to enhance overall speed and performance. + +A big shoutout to our amazing community contributors: +- [@aravindkarnam](https://github.com/aravindkarnam) for developing the textual description extraction feature. +- [@FractalMind](https://github.com/FractalMind) for creating the first official Docker Hub image and fixing Dockerfile errors. +- [@ketonkss4](https://github.com/ketonkss4) for identifying Selenium's new capabilities, helping us reduce dependencies. + +Your contributions are driving Crawl4AI forward! 🙌 + +## [v0.2.75] - 2024-07-19 + +Minor improvements for a more maintainable codebase: + +- 🔄 Fixed typos in `chunking_strategy.py` and `crawler_strategy.py` to improve code readability +- 🔄 Removed `.test_pads/` directory from `.gitignore` to keep our repository clean and organized + +These changes may seem small, but they contribute to a more stable and sustainable codebase. By fixing typos and updating our `.gitignore` settings, we're ensuring that our code is easier to maintain and scale in the long run. + + +## v0.2.74 - 2024-07-08 +A slew of exciting updates to improve the crawler's stability and robustness! 🎉 + +- 💻 **UTF encoding fix**: Resolved the Windows \"charmap\" error by adding UTF encoding. +- 🛡️ **Error handling**: Implemented MaxRetryError exception handling in LocalSeleniumCrawlerStrategy. +- 🧹 **Input sanitization**: Improved input sanitization and handled encoding issues in LLMExtractionStrategy. +- 🚮 **Database cleanup**: Removed existing database file and initialized a new one. + +## [v0.2.73] - 2024-07-03 + +💡 In this release, we've bumped the version to v0.2.73 and refreshed our documentation to ensure you have the best experience with our project. + +* Supporting website need "with-head" mode to crawl the website with head. +* Fixing the installation issues for setup.py and dockerfile. +* Resolve multiple issues. + +## [v0.2.72] - 2024-06-30 + +This release brings exciting updates and improvements to our project! 🎉 + +* 📚 **Documentation Updates**: Our documentation has been revamped to reflect the latest changes and additions. +* 🚀 **New Modes in setup.py**: We've added support for three new modes in setup.py: default, torch, and transformers. This enhances the project's flexibility and usability. +* 🐳 **Docker File Updates**: The Docker file has been updated to ensure seamless compatibility with the new modes and improvements. +* 🕷️ **Temporary Solution for Headless Crawling**: We've implemented a temporary solution to overcome issues with crawling websites in headless mode. + +These changes aim to improve the overall user experience, provide more flexibility, and enhance the project's performance. We're thrilled to share these updates with you and look forward to continuing to evolve and improve our project! + +## [0.2.71] - 2024-06-26 + +**Improved Error Handling and Performance** 🚧 + +* 🚫 Refactored `crawler_strategy.py` to handle exceptions and provide better error messages, making it more robust and reliable. +* 💻 Optimized the `get_content_of_website_optimized` function in `utils.py` for improved performance, reducing potential bottlenecks. +* 💻 Updated `utils.py` with the latest changes, ensuring consistency and accuracy. +* 🚫 Migrated to `ChromeDriverManager` to resolve Chrome driver download issues, providing a smoother user experience. + +These changes focus on refining the existing codebase, resulting in a more stable, efficient, and user-friendly experience. With these improvements, you can expect fewer errors and better performance in the crawler strategy and utility functions. + +## [0.2.71] - 2024-06-25 +### Fixed +- Speed up twice the extraction function. + +## [0.2.6] - 2024-06-22 +### Fixed +- Fix issue #19: Update Dockerfile to ensure compatibility across multiple platforms. + +## [0.2.5] - 2024-06-18 +### Added +- Added five important hooks to the crawler: + - on_driver_created: Called when the driver is ready for initializations. + - before_get_url: Called right before Selenium fetches the URL. + - after_get_url: Called after Selenium fetches the URL. + - before_return_html: Called when the data is parsed and ready. + - on_user_agent_updated: Called when the user changes the user_agent, causing the driver to reinitialize. +- Added an example in `quickstart.py` in the example folder under the docs. +- Enhancement issue #24: Replaced inline HTML tags (e.g., DEL, INS, SUB, ABBR) with textual format for better context handling in LLM. +- Maintaining the semantic context of inline tags (e.g., abbreviation, DEL, INS) for improved LLM-friendliness. +- Updated Dockerfile to ensure compatibility across multiple platforms (Hopefully!). + +## [0.2.4] - 2024-06-17 +### Fixed +- Fix issue #22: Use MD5 hash for caching HTML files to handle long URLs diff --git a/docs/md _sync/contact.md b/docs/md _sync/contact.md new file mode 100644 index 00000000..85faa217 --- /dev/null +++ b/docs/md _sync/contact.md @@ -0,0 +1,25 @@ +# Contact +If you have any questions, suggestions, or feedback, please feel free to reach out to us: + +- GitHub: [unclecode](https://github.com/unclecode) +- Twitter: [@unclecode](https://twitter.com/unclecode) +- Website: [crawl4ai.com](https://crawl4ai.com) + + +## Contributing 🤝 + +We welcome contributions from the open-source community to help improve Crawl4AI and make it even more valuable for AI enthusiasts and developers. To contribute, please follow these steps: + +1. Fork the repository. +2. Create a new branch for your feature or bug fix. +3. Make your changes and commit them with descriptive messages. +4. Push your changes to your forked repository. +5. Submit a pull request to the main repository. + +For more information on contributing, please see our [contribution guidelines](https://github.com/unclecode/crawl4ai/blob/main/CONTRIBUTING.md). + +## License 📄 + +Crawl4AI is released under the [Apache 2.0 License](https://github.com/unclecode/crawl4ai/blob/main/LICENSE). + +Let's work together to make the web more accessible and useful for AI applications! 💪🌐🤖 diff --git a/docs/md _sync/demo.md b/docs/md _sync/demo.md new file mode 100644 index 00000000..4ca6c754 --- /dev/null +++ b/docs/md _sync/demo.md @@ -0,0 +1,231 @@ +# Interactive Demo for Crowler +

+
+
+ Enter URL and Options +
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+
Loading... Please wait.
+
+ +
+

Response

+
+
    +
  • Markdown
  • +
  • Cleaned HTML
  • +
  • Media
  • +
  • Extracted Content
  • +
  • Screenshot
  • +
  • Python Code
  • +
+
+
+
+ + +
+
+
+
+ + + + + + + + + + +
+
+ + + + +
\ No newline at end of file diff --git a/docs/md _sync/examples/hooks_auth.md b/docs/md _sync/examples/hooks_auth.md new file mode 100644 index 00000000..2b4c2701 --- /dev/null +++ b/docs/md _sync/examples/hooks_auth.md @@ -0,0 +1,100 @@ +# Hooks & Auth + +Crawl4AI allows you to customize the behavior of the web crawler using hooks. Hooks are functions that are called at specific points in the crawling process, allowing you to modify the crawler's behavior or perform additional actions. This example demonstrates how to use various hooks to customize the crawling process. + +## Example: Using Crawler Hooks + +Let's see how we can customize the crawler using hooks! In this example, we'll: + +1. Maximize the browser window and log in to a website when the driver is created. +2. Add a custom header before fetching the URL. +3. Log the current URL after fetching it. +4. Log the length of the HTML before returning it. + +### Hook Definitions + +```python +from crawl4ai.web_crawler import WebCrawler +from crawl4ai.crawler_strategy import * + +def on_driver_created(driver): + print("[HOOK] on_driver_created") + # Example customization: maximize the window + driver.maximize_window() + + # Example customization: logging in to a hypothetical website + driver.get('https://example.com/login') + + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.common.by import By + from selenium.webdriver.support import expected_conditions as EC + + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.NAME, 'username')) + ) + driver.find_element(By.NAME, 'username').send_keys('testuser') + driver.find_element(By.NAME, 'password').send_keys('password123') + driver.find_element(By.NAME, 'login').click() + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.ID, 'welcome')) + ) + # Add a custom cookie + driver.add_cookie({'name': 'test_cookie', 'value': 'cookie_value'}) + return driver + + +def before_get_url(driver): + print("[HOOK] before_get_url") + # Example customization: add a custom header + # Enable Network domain for sending headers + driver.execute_cdp_cmd('Network.enable', {}) + # Add a custom header + driver.execute_cdp_cmd('Network.setExtraHTTPHeaders', {'headers': {'X-Test-Header': 'test'}}) + return driver + +def after_get_url(driver): + print("[HOOK] after_get_url") + # Example customization: log the URL + print(driver.current_url) + return driver + +def before_return_html(driver, html): + print("[HOOK] before_return_html") + # Example customization: log the HTML + print(len(html)) + return driver +``` + +### Using the Hooks with the WebCrawler + +```python +print("\n🔗 [bold cyan]Using Crawler Hooks: Let's see how we can customize the crawler using hooks![/bold cyan]", True) +crawler_strategy = LocalSeleniumCrawlerStrategy(verbose=True) +crawler_strategy.set_hook('on_driver_created', on_driver_created) +crawler_strategy.set_hook('before_get_url', before_get_url) +crawler_strategy.set_hook('after_get_url', after_get_url) +crawler_strategy.set_hook('before_return_html', before_return_html) +crawler = WebCrawler(verbose=True, crawler_strategy=crawler_strategy) +crawler.warmup() + +result = crawler.run(url="https://example.com") + +print("[LOG] 📦 [bold yellow]Crawler Hooks result:[/bold yellow]") +print(result) +``` + +### Explanation + +- `on_driver_created`: This hook is called when the Selenium driver is created. In this example, it maximizes the window, logs in to a website, and adds a custom cookie. +- `before_get_url`: This hook is called right before Selenium fetches the URL. In this example, it adds a custom HTTP header. +- `after_get_url`: This hook is called after Selenium fetches the URL. In this example, it logs the current URL. +- `before_return_html`: This hook is called before returning the HTML content. In this example, it logs the length of the HTML content. + +### Additional Ideas + +- **Add custom headers to requests**: You can add custom headers to the requests using the `before_get_url` hook. +- **Perform safety checks**: Use the hooks to perform safety checks before the crawling process starts. +- **Modify the HTML content**: Use the `before_return_html` hook to modify the HTML content before it is returned. +- **Log additional information**: Use the hooks to log additional information for debugging or monitoring purposes. + +By using these hooks, you can customize the behavior of the crawler to suit your specific needs. diff --git a/docs/md _sync/examples/index.md b/docs/md _sync/examples/index.md new file mode 100644 index 00000000..14380e3d --- /dev/null +++ b/docs/md _sync/examples/index.md @@ -0,0 +1,29 @@ +# Examples + +Welcome to the examples section of Crawl4AI documentation! In this section, you will find practical examples demonstrating how to use Crawl4AI for various web crawling and data extraction tasks. Each example is designed to showcase different features and capabilities of the library. + +## Examples Index + +### [LLM Extraction](llm_extraction.md) + +This example demonstrates how to use Crawl4AI to extract information using Large Language Models (LLMs). You will learn how to configure the `LLMExtractionStrategy` to get structured data from web pages. + +### [JS Execution & CSS Filtering](js_execution_css_filtering.md) + +Learn how to execute custom JavaScript code and filter data using CSS selectors. This example shows how to perform complex web interactions and extract specific content from web pages. + +### [Hooks & Auth](hooks_auth.md) + +This example covers the use of custom hooks for authentication and other pre-crawling tasks. You will see how to set up hooks to modify headers, authenticate sessions, and perform other preparatory actions before crawling. + +### [Summarization](summarization.md) + +Discover how to use Crawl4AI to summarize web page content. This example demonstrates the summarization capabilities of the library, helping you extract concise information from lengthy web pages. + +### [Research Assistant](research_assistant.md) + +In this example, Crawl4AI is used as a research assistant to gather and organize information from multiple sources. You will learn how to use various extraction and chunking strategies to compile a comprehensive report. + +--- + +Each example includes detailed explanations and code snippets to help you understand and implement the features in your projects. Click on the links to explore each example and start making the most of Crawl4AI! diff --git a/docs/md _sync/examples/js_execution_css_filtering.md b/docs/md _sync/examples/js_execution_css_filtering.md new file mode 100644 index 00000000..0592f094 --- /dev/null +++ b/docs/md _sync/examples/js_execution_css_filtering.md @@ -0,0 +1,44 @@ +# JS Execution & CSS Filtering + +In this example, we'll demonstrate how to use Crawl4AI to execute JavaScript, filter data with CSS selectors, and use a cosine similarity strategy to extract relevant content. This approach is particularly useful when you need to interact with dynamic content on web pages, such as clicking "Load More" buttons. + +## Example: Extracting Structured Data + +```python +# Import necessary modules +from crawl4ai import WebCrawler +from crawl4ai.chunking_strategy import * +from crawl4ai.extraction_strategy import * +from crawl4ai.crawler_strategy import * + +# Define the JavaScript code to click the "Load More" button +js_code = [""" +const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); +loadMoreButton && loadMoreButton.click(); +"""] + +crawler = WebCrawler(verbose=True) +crawler.warmup() +# Run the crawler with keyword filtering and CSS selector +result = crawler.run( + url="https://www.nbcnews.com/business", + js=js_code, + css_selector="p", + extraction_strategy=CosineStrategy( + semantic_filter="technology", + ), +) + +# Display the extracted result +print(result) +``` + +### Explanation + +1. **JavaScript Execution**: The `js_code` variable contains JavaScript code that simulates clicking a "Load More" button. This is useful for loading additional content dynamically. +2. **CSS Selector**: The `css_selector="p"` parameter ensures that only paragraph (`

`) tags are extracted from the web page. +3. **Extraction Strategy**: The `CosineStrategy` is used with a semantic filter for "technology" to extract relevant content based on cosine similarity. + +## Try It Yourself + +This example demonstrates the power and flexibility of Crawl4AI in handling complex web interactions and extracting meaningful data. You can customize the JavaScript code, CSS selectors, and extraction strategies to suit your specific requirements. diff --git a/docs/md _sync/examples/llm_extraction.md b/docs/md _sync/examples/llm_extraction.md new file mode 100644 index 00000000..b7805726 --- /dev/null +++ b/docs/md _sync/examples/llm_extraction.md @@ -0,0 +1,90 @@ +# LLM Extraction + +Crawl4AI allows you to use Language Models (LLMs) to extract structured data or relevant content from web pages. Below are two examples demonstrating how to use LLMExtractionStrategy for different purposes. + +## Example 1: Extract Structured Data + +In this example, we use the `LLMExtractionStrategy` to extract structured data (model names and their fees) from the OpenAI pricing page. + +```python +import os +import time +from crawl4ai.web_crawler import WebCrawler +from crawl4ai.chunking_strategy import * +from crawl4ai.extraction_strategy import * +from crawl4ai.crawler_strategy import * + +url = r'https://openai.com/api/pricing/' + +crawler = WebCrawler() +crawler.warmup() + +from pydantic import BaseModel, Field + +class OpenAIModelFee(BaseModel): + model_name: str = Field(..., description="Name of the OpenAI model.") + input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") + output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") + +result = crawler.run( + url=url, + word_count_threshold=1, + extraction_strategy= LLMExtractionStrategy( + provider= "openai/gpt-4o", api_token = os.getenv('OPENAI_API_KEY'), + schema=OpenAIModelFee.model_json_schema(), + extraction_type="schema", + instruction="From the crawled content, extract all mentioned model names along with their "\ + "fees for input and output tokens. Make sure not to miss anything in the entire content. "\ + 'One extracted model JSON format should look like this: '\ + '{ "model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens" }' + ), + bypass_cache=True, +) + +model_fees = json.loads(result.extracted_content) + +print(len(model_fees)) + +with open(".data/data.json", "w", encoding="utf-8") as f: + f.write(result.extracted_content) +``` + +## Example 2: Extract Relevant Content + +In this example, we instruct the LLM to extract only content related to technology from the NBC News business page. + +```python +crawler = WebCrawler() +crawler.warmup() + +result = crawler.run( + url="https://www.nbcnews.com/business", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Extract only content related to technology" + ), + bypass_cache=True, + ) + +model_fees = json.loads(result.extracted_content) + +print(len(model_fees)) + +with open(".data/data.json", "w", encoding="utf-8") as f: + f.write(result.extracted_content) +``` + +## Customizing LLM Provider + +Under the hood, Crawl4AI uses the `litellm` library, which allows you to use any LLM provider you want. Just pass the correct model name and API token. + +```python +extraction_strategy=LLMExtractionStrategy( + provider="your_llm_provider/model_name", + api_token="your_api_token", + instruction="Your extraction instruction" +) +``` + +This flexibility allows you to integrate with various LLM providers and tailor the extraction process to your specific needs. diff --git a/docs/md _sync/examples/research_assistant.md b/docs/md _sync/examples/research_assistant.md new file mode 100644 index 00000000..9284fbef --- /dev/null +++ b/docs/md _sync/examples/research_assistant.md @@ -0,0 +1,248 @@ +## Research Assistant Example + +This example demonstrates how to build a research assistant using `Chainlit` and `Crawl4AI`. The assistant will be capable of crawling web pages for information and answering questions based on the crawled content. Additionally, it integrates speech-to-text functionality for audio inputs. + +### Step-by-Step Guide + +1. **Install Required Packages** + + Ensure you have the necessary packages installed. You need `chainlit`, `groq`, `requests`, and `openai`. + + ```bash + pip install chainlit groq requests openai + ``` + +2. **Import Libraries** + + Import all the necessary modules and initialize the OpenAI client. + + ```python + import os + import time + from openai import AsyncOpenAI + import chainlit as cl + import re + import requests + from io import BytesIO + from chainlit.element import ElementBased + from groq import Groq + + from concurrent.futures import ThreadPoolExecutor + + client = AsyncOpenAI(base_url="https://api.groq.com/openai/v1", api_key=os.getenv("GROQ_API_KEY")) + + # Instrument the OpenAI client + cl.instrument_openai() + ``` + +3. **Set Configuration** + + Define the model settings for the assistant. + + ```python + settings = { + "model": "llama3-8b-8192", + "temperature": 0.5, + "max_tokens": 500, + "top_p": 1, + "frequency_penalty": 0, + "presence_penalty": 0, + } + ``` + +4. **Define Utility Functions** + + - **Extract URLs from Text**: Use regex to find URLs in messages. + + ```python + def extract_urls(text): + url_pattern = re.compile(r'(https?://\S+)') + return url_pattern.findall(text) + ``` + + - **Crawl URL**: Send a request to `Crawl4AI` to fetch the content of a URL. + + ```python + def crawl_url(url): + data = { + "urls": [url], + "include_raw_html": True, + "word_count_threshold": 10, + "extraction_strategy": "NoExtractionStrategy", + "chunking_strategy": "RegexChunking" + } + response = requests.post("https://crawl4ai.com/crawl", json=data) + response_data = response.json() + response_data = response_data['results'][0] + return response_data['markdown'] + ``` + +5. **Initialize Chat Start Event** + + Set up the initial chat message and user session. + + ```python + @cl.on_chat_start + async def on_chat_start(): + cl.user_session.set("session", { + "history": [], + "context": {} + }) + await cl.Message( + content="Welcome to the chat! How can I assist you today?" + ).send() + ``` + +6. **Handle Incoming Messages** + + Process user messages, extract URLs, and crawl them concurrently. Update the chat history and system message. + + ```python + @cl.on_message + async def on_message(message: cl.Message): + user_session = cl.user_session.get("session") + + # Extract URLs from the user's message + urls = extract_urls(message.content) + + futures = [] + with ThreadPoolExecutor() as executor: + for url in urls: + futures.append(executor.submit(crawl_url, url)) + + results = [future.result() for future in futures] + + for url, result in zip(urls, results): + ref_number = f"REF_{len(user_session['context']) + 1}" + user_session["context"][ref_number] = { + "url": url, + "content": result + } + + user_session["history"].append({ + "role": "user", + "content": message.content + }) + + # Create a system message that includes the context + context_messages = [ + f'\n{data["content"]}\n' + for ref, data in user_session["context"].items() + ] + if context_messages: + system_message = { + "role": "system", + "content": ( + "You are a helpful bot. Use the following context for answering questions. " + "Refer to the sources using the REF number in square brackets, e.g., [1], only if the source is given in the appendices below.\n\n" + "If the question requires any information from the provided appendices or context, refer to the sources. " + "If not, there is no need to add a references section. " + "At the end of your response, provide a reference section listing the URLs and their REF numbers only if sources from the appendices were used.\n\n" + "\n\n".join(context_messages) + ) + } + else: + system_message = { + "role": "system", + "content": "You are a helpful assistant." + } + + msg = cl.Message(content="") + await msg.send() + + # Get response from the LLM + stream = await client.chat.completions.create( + messages=[ + system_message, + *user_session["history"] + ], + stream=True, + **settings + ) + + assistant_response = "" + async for part in stream: + if token := part.choices[0].delta.content: + assistant_response += token + await msg.stream_token(token) + + # Add assistant message to the history + user_session["history"].append({ + "role": "assistant", + "content": assistant_response + }) + await msg.update() + + # Append the reference section to the assistant's response + reference_section = "\n\nReferences:\n" + for ref, data in user_session["context"].items(): + reference_section += f"[{ref.split('_')[1]}]: {data['url']}\n" + + msg.content += reference_section + await msg.update() + ``` + +7. **Handle Audio Input** + + Capture and transcribe audio input. Store the audio buffer and transcribe it when the audio ends. + + ```python + @cl.on_audio_chunk + async def on_audio_chunk(chunk: cl.AudioChunk): + if chunk.isStart: + buffer = BytesIO() + buffer.name = f"input_audio.{chunk.mimeType.split('/')[1]}" + cl.user_session.set("audio_buffer", buffer) + cl.user_session.set("audio_mime_type", chunk.mimeType) + + cl.user_session.get("audio_buffer").write(chunk.data) + + @cl.step(type="tool") + async def speech_to_text(audio_file): + cli = Groq() + response = await client.audio.transcriptions.create( + model="whisper-large-v3", file=audio_file + ) + return response.text + + @cl.on_audio_end + async def on_audio_end(elements: list[ElementBased]): + audio_buffer: BytesIO = cl.user_session.get("audio_buffer") + audio_buffer.seek(0) + audio_file = audio_buffer.read() + audio_mime_type: str = cl.user_session.get("audio_mime_type") + + start_time = time.time() + transcription = await speech_to_text((audio_buffer.name, audio_file, audio_mime_type)) + end_time = time.time() + print(f"Transcription took {end_time - start_time} seconds") + + user_msg = cl.Message( + author="You", + type="user_message", + content=transcription + ) + await user_msg.send() + await on_message(user_msg) + ``` + +8. **Run the Chat Application** + + Start the Chainlit application. + + ```python + if __name__ == "__main__": + from chainlit.cli import run_chainlit + run_chainlit(__file__) + ``` + +### Explanation + +- **Libraries and Configuration**: Import necessary libraries and configure the OpenAI client. +- **Utility Functions**: Define functions to extract URLs and crawl them. +- **Chat Start Event**: Initialize chat session and welcome message. +- **Message Handling**: Extract URLs, crawl them concurrently, and update chat history and context. +- **Audio Handling**: Capture, buffer, and transcribe audio input, then process the transcription as text. +- **Running the Application**: Start the Chainlit server to interact with the assistant. + +This example showcases how to create an interactive research assistant that can fetch, process, and summarize web content, along with handling audio inputs for a seamless user experience. diff --git a/docs/md _sync/examples/summarization.md b/docs/md _sync/examples/summarization.md new file mode 100644 index 00000000..b817f691 --- /dev/null +++ b/docs/md _sync/examples/summarization.md @@ -0,0 +1,108 @@ +## Summarization Example + +This example demonstrates how to use `Crawl4AI` to extract a summary from a web page. The goal is to obtain the title, a detailed summary, a brief summary, and a list of keywords from the given page. + +### Step-by-Step Guide + +1. **Import Necessary Modules** + + First, import the necessary modules and classes. + + ```python + import os + import time + import json + from crawl4ai.web_crawler import WebCrawler + from crawl4ai.chunking_strategy import * + from crawl4ai.extraction_strategy import * + from crawl4ai.crawler_strategy import * + from pydantic import BaseModel, Field + ``` + +2. **Define the URL to be Crawled** + + Set the URL of the web page you want to summarize. + + ```python + url = r'https://marketplace.visualstudio.com/items?itemName=Unclecode.groqopilot' + ``` + +3. **Initialize the WebCrawler** + + Create an instance of the `WebCrawler` and call the `warmup` method. + + ```python + crawler = WebCrawler() + crawler.warmup() + ``` + +4. **Define the Data Model** + + Use Pydantic to define the structure of the extracted data. + + ```python + class PageSummary(BaseModel): + title: str = Field(..., description="Title of the page.") + summary: str = Field(..., description="Summary of the page.") + brief_summary: str = Field(..., description="Brief summary of the page.") + keywords: list = Field(..., description="Keywords assigned to the page.") + ``` + +5. **Run the Crawler** + + Set up and run the crawler with the `LLMExtractionStrategy`. Provide the necessary parameters, including the schema for the extracted data and the instruction for the LLM. + + ```python + result = crawler.run( + url=url, + word_count_threshold=1, + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + schema=PageSummary.model_json_schema(), + extraction_type="schema", + apply_chunking=False, + instruction=( + "From the crawled content, extract the following details: " + "1. Title of the page " + "2. Summary of the page, which is a detailed summary " + "3. Brief summary of the page, which is a paragraph text " + "4. Keywords assigned to the page, which is a list of keywords. " + 'The extracted JSON format should look like this: ' + '{ "title": "Page Title", "summary": "Detailed summary of the page.", ' + '"brief_summary": "Brief summary in a paragraph.", "keywords": ["keyword1", "keyword2", "keyword3"] }' + ) + ), + bypass_cache=True, + ) + ``` + +6. **Process the Extracted Data** + + Load the extracted content into a JSON object and print it. + + ```python + page_summary = json.loads(result.extracted_content) + print(page_summary) + ``` + +7. **Save the Extracted Data** + + Save the extracted data to a file for further use. + + ```python + with open(".data/page_summary.json", "w", encoding="utf-8") as f: + f.write(result.extracted_content) + ``` + +### Explanation + +- **Importing Modules**: Import the necessary modules, including `WebCrawler` and `LLMExtractionStrategy` from `Crawl4AI`. +- **URL Definition**: Set the URL of the web page you want to crawl and summarize. +- **WebCrawler Initialization**: Create an instance of `WebCrawler` and call the `warmup` method to prepare the crawler. +- **Data Model Definition**: Define the structure of the data you want to extract using Pydantic's `BaseModel`. +- **Crawler Execution**: Run the crawler with the `LLMExtractionStrategy`, providing the schema and detailed instructions for the extraction process. +- **Data Processing**: Load the extracted content into a JSON object and print it to verify the results. +- **Data Saving**: Save the extracted data to a file for further use. + +This example demonstrates how to harness the power of `Crawl4AI` to perform advanced web crawling and data extraction tasks with minimal code. diff --git a/docs/md _sync/full_details/advanced_features.md b/docs/md _sync/full_details/advanced_features.md new file mode 100644 index 00000000..944f6e0d --- /dev/null +++ b/docs/md _sync/full_details/advanced_features.md @@ -0,0 +1,138 @@ +# Advanced Features + +Crawl4AI offers a range of advanced features that allow you to fine-tune your web crawling and data extraction process. This section will cover some of these advanced features, including taking screenshots, extracting media and links, customizing the user agent, using custom hooks, and leveraging CSS selectors. + +## Taking Screenshots 📸 + +One of the cool features of Crawl4AI is the ability to take screenshots of the web pages you're crawling. This can be particularly useful for visual verification or for capturing the state of dynamic content. + +Here's how you can take a screenshot: + +```python +from crawl4ai import WebCrawler +import base64 + +# Create the WebCrawler instance +crawler = WebCrawler() +crawler.warmup() + +# Run the crawler with the screenshot parameter +result = crawler.run(url="https://www.nbcnews.com/business", screenshot=True) + +# Save the screenshot to a file +with open("screenshot.png", "wb") as f: + f.write(base64.b64decode(result.screenshot)) + +print("Screenshot saved to 'screenshot.png'!") +``` + +In this example, we create a `WebCrawler` instance, warm it up, and then run it with the `screenshot` parameter set to `True`. The screenshot is saved as a base64 encoded string in the result, which we then decode and save as a PNG file. + +## Extracting Media and Links 🎨🔗 + +Crawl4AI can extract all media tags (images, audio, and video) and links (both internal and external) from a web page. This feature is useful for collecting multimedia content or analyzing link structures. + +Here's an example: + +```python +from crawl4ai import WebCrawler + +# Create the WebCrawler instance +crawler = WebCrawler() +crawler.warmup() + +# Run the crawler +result = crawler.run(url="https://www.nbcnews.com/business") + +print("Extracted media:", result.media) +print("Extracted links:", result.links) +``` + +In this example, the `result` object contains dictionaries for media and links, which you can access and use as needed. + +## Customizing the User Agent 🕵️‍♂️ + +Crawl4AI allows you to set a custom user agent for your HTTP requests. This can help you avoid detection by web servers or simulate different browsing environments. + +Here's how to set a custom user agent: + +```python +from crawl4ai import WebCrawler + +# Create the WebCrawler instance +crawler = WebCrawler() +crawler.warmup() + +# Run the crawler with a custom user agent +result = crawler.run(url="https://www.nbcnews.com/business", user_agent="Mozilla/5.0 (compatible; MyCrawler/1.0)") + +print("Crawl result:", result) +``` + +In this example, we specify a custom user agent string when running the crawler. + +## Using Custom Hooks 🪝 + +Hooks are a powerful feature in Crawl4AI that allow you to customize the crawling process at various stages. You can define hooks for actions such as driver initialization, before and after URL fetching, and before returning the HTML. + +Here's an example of using hooks: + +```python +from crawl4ai import WebCrawler +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +# Define the hooks +def on_driver_created(driver): + driver.maximize_window() + driver.get('https://example.com/login') + WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.NAME, 'username'))).send_keys('testuser') + driver.find_element(By.NAME, 'password').send_keys('password123') + driver.find_element(By.NAME, 'login').click() + return driver + +def before_get_url(driver): + driver.execute_cdp_cmd('Network.setExtraHTTPHeaders', {'headers': {'X-Test-Header': 'test'}}) + return driver + +# Create the WebCrawler instance +crawler = WebCrawler() +crawler.warmup() + +# Set the hooks +crawler.set_hook('on_driver_created', on_driver_created) +crawler.set_hook('before_get_url', before_get_url) + +# Run the crawler +result = crawler.run(url="https://example.com") + +print("Crawl result:", result) +``` + +In this example, we define hooks to handle driver initialization and custom headers before fetching the URL. + +## Using CSS Selectors 🎯 + +CSS selectors allow you to target specific elements on a web page for extraction. This can be useful for scraping structured content, such as articles or product details. + +Here's an example of using a CSS selector: + +```python +from crawl4ai import WebCrawler + +# Create the WebCrawler instance +crawler = WebCrawler() +crawler.warmup() + +# Run the crawler with a CSS selector to extract only H2 tags +result = crawler.run(url="https://www.nbcnews.com/business", css_selector="h2") + +print("Extracted H2 tags:", result.extracted_content) +``` + +In this example, we use the `css_selector` parameter to extract only the H2 tags from the web page. + +--- + +With these advanced features, you can leverage Crawl4AI to perform sophisticated web crawling and data extraction tasks. Whether you need to take screenshots, extract specific elements, customize the crawling process, or set custom headers, Crawl4AI provides the flexibility and power to meet your needs. Happy crawling! 🕷️🚀 diff --git a/docs/md _sync/full_details/chunking_strategies.md b/docs/md _sync/full_details/chunking_strategies.md new file mode 100644 index 00000000..f429310f --- /dev/null +++ b/docs/md _sync/full_details/chunking_strategies.md @@ -0,0 +1,133 @@ +## Chunking Strategies 📚 + +Crawl4AI provides several powerful chunking strategies to divide text into manageable parts for further processing. Each strategy has unique characteristics and is suitable for different scenarios. Let's explore them one by one. + +### RegexChunking + +`RegexChunking` splits text using regular expressions. This is ideal for creating chunks based on specific patterns like paragraphs or sentences. + +#### When to Use +- Great for structured text with consistent delimiters. +- Suitable for documents where specific patterns (e.g., double newlines, periods) indicate logical chunks. + +#### Parameters +- `patterns` (list, optional): Regular expressions used to split the text. Default is to split by double newlines (`['\n\n']`). + +#### Example +```python +from crawl4ai.chunking_strategy import RegexChunking + +# Define patterns for splitting text +patterns = [r'\n\n', r'\. '] +chunker = RegexChunking(patterns=patterns) + +# Sample text +text = "This is a sample text. It will be split into chunks.\n\nThis is another paragraph." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +### NlpSentenceChunking + +`NlpSentenceChunking` uses NLP models to split text into sentences, ensuring accurate sentence boundaries. + +#### When to Use +- Ideal for texts where sentence boundaries are crucial. +- Useful for creating chunks that preserve grammatical structures. + +#### Parameters +- None. + +#### Example +```python +from crawl4ai.chunking_strategy import NlpSentenceChunking + +chunker = NlpSentenceChunking() + +# Sample text +text = "This is a sample text. It will be split into sentences. Here's another sentence." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +### TopicSegmentationChunking + +`TopicSegmentationChunking` employs the TextTiling algorithm to segment text into topic-based chunks. This method identifies thematic boundaries. + +#### When to Use +- Perfect for long documents with distinct topics. +- Useful when preserving topic continuity is more important than maintaining text order. + +#### Parameters +- `num_keywords` (int, optional): Number of keywords for each topic segment. Default is `3`. + +#### Example +```python +from crawl4ai.chunking_strategy import TopicSegmentationChunking + +chunker = TopicSegmentationChunking(num_keywords=3) + +# Sample text +text = "This document contains several topics. Topic one discusses AI. Topic two covers machine learning." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +### FixedLengthWordChunking + +`FixedLengthWordChunking` splits text into chunks based on a fixed number of words. This ensures each chunk has approximately the same length. + +#### When to Use +- Suitable for processing large texts where uniform chunk size is important. +- Useful when the number of words per chunk needs to be controlled. + +#### Parameters +- `chunk_size` (int, optional): Number of words per chunk. Default is `100`. + +#### Example +```python +from crawl4ai.chunking_strategy import FixedLengthWordChunking + +chunker = FixedLengthWordChunking(chunk_size=10) + +# Sample text +text = "This is a sample text. It will be split into chunks of fixed length." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +### SlidingWindowChunking + +`SlidingWindowChunking` uses a sliding window approach to create overlapping chunks. Each chunk has a fixed length, and the window slides by a specified step size. + +#### When to Use +- Ideal for creating overlapping chunks to preserve context. +- Useful for tasks where context from adjacent chunks is needed. + +#### Parameters +- `window_size` (int, optional): Number of words in each chunk. Default is `100`. +- `step` (int, optional): Number of words to slide the window. Default is `50`. + +#### Example +```python +from crawl4ai.chunking_strategy import SlidingWindowChunking + +chunker = SlidingWindowChunking(window_size=10, step=5) + +# Sample text +text = "This is a sample text. It will be split using a sliding window approach to preserve context." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +With these chunking strategies, you can choose the best method to divide your text based on your specific needs. Whether you need precise sentence boundaries, topic-based segmentation, or uniform chunk sizes, Crawl4AI has you covered. Happy chunking! 📝✨ diff --git a/docs/md _sync/full_details/crawl_request_parameters.md b/docs/md _sync/full_details/crawl_request_parameters.md new file mode 100644 index 00000000..c32743c6 --- /dev/null +++ b/docs/md _sync/full_details/crawl_request_parameters.md @@ -0,0 +1,130 @@ +# Crawl Request Parameters + +The `run` function in Crawl4AI is designed to be highly configurable, allowing you to customize the crawling and extraction process to suit your needs. Below are the parameters you can use with the `run` function, along with their descriptions, possible values, and examples. + +## Parameters + +### url (str) +**Description:** The URL of the webpage to crawl. +**Required:** Yes +**Example:** +```python +url = "https://www.nbcnews.com/business" +``` + +### word_count_threshold (int) +**Description:** The minimum number of words a block must contain to be considered meaningful. The default value is `5`. +**Required:** No +**Default Value:** `5` +**Example:** +```python +word_count_threshold = 10 +``` + +### extraction_strategy (ExtractionStrategy) +**Description:** The strategy to use for extracting content from the HTML. It must be an instance of `ExtractionStrategy`. If not provided, the default is `NoExtractionStrategy`. +**Required:** No +**Default Value:** `NoExtractionStrategy()` +**Example:** +```python +extraction_strategy = CosineStrategy(semantic_filter="finance") +``` + +### chunking_strategy (ChunkingStrategy) +**Description:** The strategy to use for chunking the text before processing. It must be an instance of `ChunkingStrategy`. The default value is `RegexChunking()`. +**Required:** No +**Default Value:** `RegexChunking()` +**Example:** +```python +chunking_strategy = NlpSentenceChunking() +``` + +### bypass_cache (bool) +**Description:** Whether to force a fresh crawl even if the URL has been previously crawled. The default value is `False`. +**Required:** No +**Default Value:** `False` +**Example:** +```python +bypass_cache = True +``` + +### css_selector (str) +**Description:** The CSS selector to target specific parts of the HTML for extraction. If not provided, the entire HTML will be processed. +**Required:** No +**Default Value:** `None` +**Example:** +```python +css_selector = "div.article-content" +``` + +### screenshot (bool) +**Description:** Whether to take screenshots of the page. The default value is `False`. +**Required:** No +**Default Value:** `False` +**Example:** +```python +screenshot = True +``` + +### user_agent (str) +**Description:** The user agent to use for the HTTP requests. If not provided, a default user agent will be used. +**Required:** No +**Default Value:** `None` +**Example:** +```python +user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3" +``` + +### verbose (bool) +**Description:** Whether to enable verbose logging. The default value is `True`. +**Required:** No +**Default Value:** `True` +**Example:** +```python +verbose = True +``` + +### **kwargs +Additional keyword arguments that can be passed to customize the crawling process further. Some notable options include: + +- **only_text (bool):** Whether to extract only text content, excluding HTML tags. Default is `False`. + +**Example:** +```python +result = crawler.run( + url="https://www.nbcnews.com/business", + css_selector="p", + only_text=True +) +``` + +## Example Usage + +Here's an example of how to use the `run` function with various parameters: + +```python +from crawl4ai import WebCrawler +from crawl4ai.extraction_strategy import CosineStrategy +from crawl4ai.chunking_strategy import NlpSentenceChunking + +# Create the WebCrawler instance +crawler = WebCrawler() + +# Run the crawler with custom parameters +result = crawler.run( + url="https://www.nbcnews.com/business", + word_count_threshold=10, + extraction_strategy=CosineStrategy(semantic_filter="finance"), + chunking_strategy=NlpSentenceChunking(), + bypass_cache=True, + css_selector="div.article-content", + screenshot=True, + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", + verbose=True, + only_text=True +) + +print(result) +``` + +This example demonstrates how to configure various parameters to customize the crawling and extraction process using Crawl4AI. diff --git a/docs/md _sync/full_details/crawl_result_class.md b/docs/md _sync/full_details/crawl_result_class.md new file mode 100644 index 00000000..f66e16dc --- /dev/null +++ b/docs/md _sync/full_details/crawl_result_class.md @@ -0,0 +1,120 @@ +# Crawl Result + +The `CrawlResult` class is the heart of Crawl4AI's output, encapsulating all the data extracted from a crawling session. This class contains various fields that store the results of the web crawling and extraction process. Let's break down each field and see what it holds. 🎉 + +## Class Definition + +```python +class CrawlResult(BaseModel): + url: str + html: str + success: bool + cleaned_html: Optional[str] = None + media: Dict[str, List[Dict]] = {} + links: Dict[str, List[Dict]] = {} + screenshot: Optional[str] = None + markdown: Optional[str] = None + extracted_content: Optional[str] = None + metadata: Optional[dict] = None + error_message: Optional[str] = None +``` + +## Fields Explanation + +### `url: str` +The URL that was crawled. This field simply stores the URL of the web page that was processed. + +### `html: str` +The raw HTML content of the web page. This is the unprocessed HTML source as retrieved by the crawler. + +### `success: bool` +A flag indicating whether the crawling and extraction were successful. If any error occurs during the process, this will be `False`. + +### `cleaned_html: Optional[str]` +The cleaned HTML content of the web page. This field holds the HTML after removing unwanted tags like ` diff --git a/docs/md _sync/introduction.md b/docs/md _sync/introduction.md new file mode 100644 index 00000000..6d1ad56b --- /dev/null +++ b/docs/md _sync/introduction.md @@ -0,0 +1,29 @@ +# Introduction + +Welcome to the documentation for Crawl4AI v0.2.5! 🕷️🤖 + +Crawl4AI is designed to simplify the process of crawling web pages and extracting useful information for large language models (LLMs) and AI applications. Whether you're using it as a REST API, a Python library, or through a Google Colab notebook, Crawl4AI provides powerful features to make web data extraction easier and more efficient. + +## Key Features ✨ + +- **🆓 Completely Free and Open-Source**: Crawl4AI is free to use and open-source, making it accessible for everyone. +- **🤖 LLM-Friendly Output Formats**: Supports JSON, cleaned HTML, and markdown formats. +- **🌍 Concurrent Crawling**: Crawl multiple URLs simultaneously to save time. +- **🎨 Media Extraction**: Extract all media tags including images, audio, and video. +- **🔗 Link Extraction**: Extract all external and internal links from web pages. +- **📚 Metadata Extraction**: Extract metadata from web pages for additional context. +- **🔄 Custom Hooks**: Define custom hooks for authentication, headers, and page modifications before crawling. +- **🕵️ User Agent Support**: Customize the user agent for HTTP requests. +- **🖼️ Screenshot Capability**: Take screenshots of web pages during crawling. +- **📜 JavaScript Execution**: Execute custom JavaScripts before crawling. +- **📚 Advanced Chunking and Extraction Strategies**: Utilize topic-based, regex, sentence chunking, cosine clustering, and LLM extraction strategies. +- **🎯 CSS Selector Support**: Extract specific content using CSS selectors. +- **📝 Instruction/Keyword Refinement**: Pass instructions or keywords to refine the extraction process. + +Check the [Changelog](https://github.com/unclecode/crawl4ai/blob/main/CHANGELOG.md) for more details. + +## Power and Simplicity of Crawl4AI 🚀 + +Crawl4AI provides an easy way to crawl and extract data from web pages without installing any library. You can use the REST API on our server or run the local server on your machine. For more advanced control, use the Python library to customize your crawling and extraction strategies. + +Explore the documentation to learn more about the features, installation process, usage examples, and how to contribute to Crawl4AI. Let's make the web more accessible and useful for AI applications! 💪🌐🤖 diff --git a/docs/md _sync/quickstart.md b/docs/md _sync/quickstart.md new file mode 100644 index 00000000..a0c1a2c7 --- /dev/null +++ b/docs/md _sync/quickstart.md @@ -0,0 +1,204 @@ +# Quick Start Guide 🚀 + +Welcome to the Crawl4AI Quickstart Guide! In this tutorial, we'll walk you through the basic usage of Crawl4AI with a friendly and humorous tone. We'll cover everything from basic usage to advanced features like chunking and extraction strategies. Let's dive in! 🌟 + +## Getting Started 🛠️ + +First, let's create an instance of `WebCrawler` and call the `warmup()` function. This might take a few seconds the first time you run Crawl4AI, as it loads the required model files. + +```python +from crawl4ai import WebCrawler + +def create_crawler(): + crawler = WebCrawler(verbose=True) + crawler.warmup() + return crawler + +crawler = create_crawler() +``` + +### Basic Usage + +Simply provide a URL and let Crawl4AI do the magic! + +```python +result = crawler.run(url="https://www.nbcnews.com/business") +print(f"Basic crawl result: {result}") +``` + +### Taking Screenshots 📸 + +Let's take a screenshot of the page! + +```python +result = crawler.run(url="https://www.nbcnews.com/business", screenshot=True) +with open("screenshot.png", "wb") as f: + f.write(base64.b64decode(result.screenshot)) +print("Screenshot saved to 'screenshot.png'!") +``` + +### Understanding Parameters 🧠 + +By default, Crawl4AI caches the results of your crawls. This means that subsequent crawls of the same URL will be much faster! Let's see this in action. + +First crawl (caches the result): +```python +result = crawler.run(url="https://www.nbcnews.com/business") +print(f"First crawl result: {result}") +``` + +Force to crawl again: +```python +result = crawler.run(url="https://www.nbcnews.com/business", bypass_cache=True) +print(f"Second crawl result: {result}") +``` + +### Adding a Chunking Strategy 🧩 + +Let's add a chunking strategy: `RegexChunking`! This strategy splits the text based on a given regex pattern. + +```python +from crawl4ai.chunking_strategy import RegexChunking + +result = crawler.run( + url="https://www.nbcnews.com/business", + chunking_strategy=RegexChunking(patterns=["\n\n"]) +) +print(f"RegexChunking result: {result}") +``` + +You can also use `NlpSentenceChunking` which splits the text into sentences using NLP techniques. + +```python +from crawl4ai.chunking_strategy import NlpSentenceChunking + +result = crawler.run( + url="https://www.nbcnews.com/business", + chunking_strategy=NlpSentenceChunking() +) +print(f"NlpSentenceChunking result: {result}") +``` + +### Adding an Extraction Strategy 🧠 + +Let's get smarter with an extraction strategy: `CosineStrategy`! This strategy uses cosine similarity to extract semantically similar blocks of text. + +```python +from crawl4ai.extraction_strategy import CosineStrategy + +result = crawler.run( + url="https://www.nbcnews.com/business", + extraction_strategy=CosineStrategy( + word_count_threshold=10, + max_dist=0.2, + linkage_method="ward", + top_k=3 + ) +) +print(f"CosineStrategy result: {result}") +``` + +You can also pass other parameters like `semantic_filter` to extract specific content. + +```python +result = crawler.run( + url="https://www.nbcnews.com/business", + extraction_strategy=CosineStrategy( + semantic_filter="inflation rent prices" + ) +) +print(f"CosineStrategy result with semantic filter: {result}") +``` + +### Using LLMExtractionStrategy 🤖 + +Time to bring in the big guns: `LLMExtractionStrategy` without instructions! This strategy uses a large language model to extract relevant information from the web page. + +```python +from crawl4ai.extraction_strategy import LLMExtractionStrategy +import os + +result = crawler.run( + url="https://www.nbcnews.com/business", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY') + ) +) +print(f"LLMExtractionStrategy (no instructions) result: {result}") +``` + +You can also provide specific instructions to guide the extraction. + +```python +result = crawler.run( + url="https://www.nbcnews.com/business", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="I am interested in only financial news" + ) +) +print(f"LLMExtractionStrategy (with instructions) result: {result}") +``` + +### Targeted Extraction 🎯 + +Let's use a CSS selector to extract only H2 tags! + +```python +result = crawler.run( + url="https://www.nbcnews.com/business", + css_selector="h2" +) +print(f"CSS Selector (H2 tags) result: {result}") +``` + +### Interactive Extraction 🖱️ + +Passing JavaScript code to click the 'Load More' button! + +```python +js_code = """ +const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); +loadMoreButton && loadMoreButton.click(); +""" + +result = crawler.run( + url="https://www.nbcnews.com/business", + js=js_code +) +print(f"JavaScript Code (Load More button) result: {result}") +``` + +### Using Crawler Hooks 🔗 + +Let's see how we can customize the crawler using hooks! + +```python +import time + +from crawl4ai.web_crawler import WebCrawler +from crawl4ai.crawler_strategy import * + +def delay(driver): + print("Delaying for 5 seconds...") + time.sleep(5) + print("Resuming...") + +def create_crawler(): + crawler_strategy = LocalSeleniumCrawlerStrategy(verbose=True) + crawler_strategy.set_hook('after_get_url', delay) + crawler = WebCrawler(verbose=True, crawler_strategy=crawler_strategy) + crawler.warmup() + return crawler + +crawler = create_crawler() +result = crawler.run(url="https://www.nbcnews.com/business", bypass_cache=True) +``` + +check [Hooks](examples/hooks_auth.md) for more examples. + +## Congratulations! 🎉 + +You've made it through the Crawl4AI Quickstart Guide! Now go forth and crawl the web like a pro! 🕸️ diff --git a/docs/md/examples/hooks_auth.md b/docs/md/examples/hooks_auth.md index 2b4c2701..e4b7d7ce 100644 --- a/docs/md/examples/hooks_auth.md +++ b/docs/md/examples/hooks_auth.md @@ -1,100 +1,110 @@ -# Hooks & Auth +# Hooks & Auth for AsyncWebCrawler -Crawl4AI allows you to customize the behavior of the web crawler using hooks. Hooks are functions that are called at specific points in the crawling process, allowing you to modify the crawler's behavior or perform additional actions. This example demonstrates how to use various hooks to customize the crawling process. +Crawl4AI's AsyncWebCrawler allows you to customize the behavior of the web crawler using hooks. Hooks are asynchronous functions that are called at specific points in the crawling process, allowing you to modify the crawler's behavior or perform additional actions. This example demonstrates how to use various hooks to customize the asynchronous crawling process. -## Example: Using Crawler Hooks +## Example: Using Crawler Hooks with AsyncWebCrawler -Let's see how we can customize the crawler using hooks! In this example, we'll: +Let's see how we can customize the AsyncWebCrawler using hooks! In this example, we'll: -1. Maximize the browser window and log in to a website when the driver is created. -2. Add a custom header before fetching the URL. -3. Log the current URL after fetching it. -4. Log the length of the HTML before returning it. +1. Configure the browser when it's created. +2. Add custom headers before navigating to the URL. +3. Log the current URL after navigation. +4. Perform actions after JavaScript execution. +5. Log the length of the HTML before returning it. ### Hook Definitions ```python -from crawl4ai.web_crawler import WebCrawler -from crawl4ai.crawler_strategy import * +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy +from playwright.async_api import Page, Browser -def on_driver_created(driver): - print("[HOOK] on_driver_created") - # Example customization: maximize the window - driver.maximize_window() +async def on_browser_created(browser: Browser): + print("[HOOK] on_browser_created") + # Example customization: set browser viewport size + context = await browser.new_context(viewport={'width': 1920, 'height': 1080}) + page = await context.new_page() # Example customization: logging in to a hypothetical website - driver.get('https://example.com/login') + await page.goto('https://example.com/login') + await page.fill('input[name="username"]', 'testuser') + await page.fill('input[name="password"]', 'password123') + await page.click('button[type="submit"]') + await page.wait_for_selector('#welcome') - from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.common.by import By - from selenium.webdriver.support import expected_conditions as EC - - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.NAME, 'username')) - ) - driver.find_element(By.NAME, 'username').send_keys('testuser') - driver.find_element(By.NAME, 'password').send_keys('password123') - driver.find_element(By.NAME, 'login').click() - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.ID, 'welcome')) - ) # Add a custom cookie - driver.add_cookie({'name': 'test_cookie', 'value': 'cookie_value'}) - return driver + await context.add_cookies([{'name': 'test_cookie', 'value': 'cookie_value', 'url': 'https://example.com'}]) + await page.close() + await context.close() -def before_get_url(driver): - print("[HOOK] before_get_url") - # Example customization: add a custom header - # Enable Network domain for sending headers - driver.execute_cdp_cmd('Network.enable', {}) - # Add a custom header - driver.execute_cdp_cmd('Network.setExtraHTTPHeaders', {'headers': {'X-Test-Header': 'test'}}) - return driver +async def before_goto(page: Page): + print("[HOOK] before_goto") + # Example customization: add custom headers + await page.set_extra_http_headers({'X-Test-Header': 'test'}) -def after_get_url(driver): - print("[HOOK] after_get_url") +async def after_goto(page: Page): + print("[HOOK] after_goto") # Example customization: log the URL - print(driver.current_url) - return driver + print(f"Current URL: {page.url}") -def before_return_html(driver, html): +async def on_execution_started(page: Page): + print("[HOOK] on_execution_started") + # Example customization: perform actions after JS execution + await page.evaluate("console.log('Custom JS executed')") + +async def before_return_html(page: Page, html: str): print("[HOOK] before_return_html") - # Example customization: log the HTML - print(len(html)) - return driver + # Example customization: log the HTML length + print(f"HTML length: {len(html)}") + return page ``` -### Using the Hooks with the WebCrawler +### Using the Hooks with the AsyncWebCrawler ```python -print("\n🔗 [bold cyan]Using Crawler Hooks: Let's see how we can customize the crawler using hooks![/bold cyan]", True) -crawler_strategy = LocalSeleniumCrawlerStrategy(verbose=True) -crawler_strategy.set_hook('on_driver_created', on_driver_created) -crawler_strategy.set_hook('before_get_url', before_get_url) -crawler_strategy.set_hook('after_get_url', after_get_url) -crawler_strategy.set_hook('before_return_html', before_return_html) -crawler = WebCrawler(verbose=True, crawler_strategy=crawler_strategy) -crawler.warmup() +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy -result = crawler.run(url="https://example.com") +async def main(): + print("\n🔗 Using Crawler Hooks: Let's see how we can customize the AsyncWebCrawler using hooks!") + + crawler_strategy = AsyncPlaywrightCrawlerStrategy(verbose=True) + crawler_strategy.set_hook('on_browser_created', on_browser_created) + crawler_strategy.set_hook('before_goto', before_goto) + crawler_strategy.set_hook('after_goto', after_goto) + crawler_strategy.set_hook('on_execution_started', on_execution_started) + crawler_strategy.set_hook('before_return_html', before_return_html) + + async with AsyncWebCrawler(verbose=True, crawler_strategy=crawler_strategy) as crawler: + result = await crawler.arun( + url="https://example.com", + js_code="window.scrollTo(0, document.body.scrollHeight);", + wait_for="footer" + ) -print("[LOG] 📦 [bold yellow]Crawler Hooks result:[/bold yellow]") -print(result) + print("📦 Crawler Hooks result:") + print(result) + +asyncio.run(main()) ``` ### Explanation -- `on_driver_created`: This hook is called when the Selenium driver is created. In this example, it maximizes the window, logs in to a website, and adds a custom cookie. -- `before_get_url`: This hook is called right before Selenium fetches the URL. In this example, it adds a custom HTTP header. -- `after_get_url`: This hook is called after Selenium fetches the URL. In this example, it logs the current URL. -- `before_return_html`: This hook is called before returning the HTML content. In this example, it logs the length of the HTML content. +- `on_browser_created`: This hook is called when the Playwright browser is created. It sets up the browser context, logs in to a website, and adds a custom cookie. +- `before_goto`: This hook is called right before Playwright navigates to the URL. It adds custom HTTP headers. +- `after_goto`: This hook is called after Playwright navigates to the URL. It logs the current URL. +- `on_execution_started`: This hook is called after any custom JavaScript is executed. It performs additional JavaScript actions. +- `before_return_html`: This hook is called before returning the HTML content. It logs the length of the HTML content. ### Additional Ideas -- **Add custom headers to requests**: You can add custom headers to the requests using the `before_get_url` hook. -- **Perform safety checks**: Use the hooks to perform safety checks before the crawling process starts. -- **Modify the HTML content**: Use the `before_return_html` hook to modify the HTML content before it is returned. -- **Log additional information**: Use the hooks to log additional information for debugging or monitoring purposes. +- **Handling authentication**: Use the `on_browser_created` hook to handle login processes or set authentication tokens. +- **Dynamic header modification**: Modify headers based on the target URL or other conditions in the `before_goto` hook. +- **Content verification**: Use the `after_goto` hook to verify that the expected content is present on the page. +- **Custom JavaScript injection**: Inject and execute custom JavaScript using the `on_execution_started` hook. +- **Content preprocessing**: Modify or analyze the HTML content in the `before_return_html` hook before it's returned. -By using these hooks, you can customize the behavior of the crawler to suit your specific needs. +By using these hooks, you can customize the behavior of the AsyncWebCrawler to suit your specific needs, including handling authentication, modifying requests, and preprocessing content. \ No newline at end of file diff --git a/docs/md/examples/index.md b/docs/md/examples/index.md index 14380e3d..a6634159 100644 --- a/docs/md/examples/index.md +++ b/docs/md/examples/index.md @@ -8,6 +8,10 @@ Welcome to the examples section of Crawl4AI documentation! In this section, you This example demonstrates how to use Crawl4AI to extract information using Large Language Models (LLMs). You will learn how to configure the `LLMExtractionStrategy` to get structured data from web pages. +### [JSON CSS Extraction](json_css_extraction.md) + +This example demonstrates how to use Crawl4AI to extract structured data without using LLM, and just focusing on page structure. You will learn how to use the `JsonCssExtractionStrategy` to extract data using CSS selectors. + ### [JS Execution & CSS Filtering](js_execution_css_filtering.md) Learn how to execute custom JavaScript code and filter data using CSS selectors. This example shows how to perform complex web interactions and extract specific content from web pages. diff --git a/docs/md/examples/js_execution_css_filtering.md b/docs/md/examples/js_execution_css_filtering.md index 0592f094..b45a6602 100644 --- a/docs/md/examples/js_execution_css_filtering.md +++ b/docs/md/examples/js_execution_css_filtering.md @@ -1,44 +1,104 @@ -# JS Execution & CSS Filtering +# JS Execution & CSS Filtering with AsyncWebCrawler -In this example, we'll demonstrate how to use Crawl4AI to execute JavaScript, filter data with CSS selectors, and use a cosine similarity strategy to extract relevant content. This approach is particularly useful when you need to interact with dynamic content on web pages, such as clicking "Load More" buttons. +In this example, we'll demonstrate how to use Crawl4AI's AsyncWebCrawler to execute JavaScript, filter data with CSS selectors, and use a cosine similarity strategy to extract relevant content. This approach is particularly useful when you need to interact with dynamic content on web pages, such as clicking "Load More" buttons. -## Example: Extracting Structured Data +## Example: Extracting Structured Data Asynchronously ```python -# Import necessary modules -from crawl4ai import WebCrawler -from crawl4ai.chunking_strategy import * -from crawl4ai.extraction_strategy import * -from crawl4ai.crawler_strategy import * +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.chunking_strategy import RegexChunking +from crawl4ai.extraction_strategy import CosineStrategy +from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy -# Define the JavaScript code to click the "Load More" button -js_code = [""" -const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); -loadMoreButton && loadMoreButton.click(); -"""] +async def main(): + # Define the JavaScript code to click the "Load More" button + js_code = """ + const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); + if (loadMoreButton) { + loadMoreButton.click(); + // Wait for new content to load + await new Promise(resolve => setTimeout(resolve, 2000)); + } + """ -crawler = WebCrawler(verbose=True) -crawler.warmup() -# Run the crawler with keyword filtering and CSS selector -result = crawler.run( - url="https://www.nbcnews.com/business", - js=js_code, - css_selector="p", - extraction_strategy=CosineStrategy( - semantic_filter="technology", - ), -) + # Define a wait_for function to ensure content is loaded + wait_for = """ + () => { + const articles = document.querySelectorAll('article.tease-card'); + return articles.length > 10; + } + """ -# Display the extracted result -print(result) + async with AsyncWebCrawler(verbose=True) as crawler: + # Run the crawler with keyword filtering and CSS selector + result = await crawler.arun( + url="https://www.nbcnews.com/business", + js_code=js_code, + wait_for=wait_for, + css_selector="article.tease-card", + extraction_strategy=CosineStrategy( + semantic_filter="technology", + ), + chunking_strategy=RegexChunking(), + ) + + # Display the extracted result + print(result.extracted_content) + +# Run the async function +asyncio.run(main()) ``` ### Explanation -1. **JavaScript Execution**: The `js_code` variable contains JavaScript code that simulates clicking a "Load More" button. This is useful for loading additional content dynamically. -2. **CSS Selector**: The `css_selector="p"` parameter ensures that only paragraph (`

`) tags are extracted from the web page. -3. **Extraction Strategy**: The `CosineStrategy` is used with a semantic filter for "technology" to extract relevant content based on cosine similarity. +1. **Asynchronous Execution**: We use `AsyncWebCrawler` with async/await syntax for non-blocking execution. + +2. **JavaScript Execution**: The `js_code` variable contains JavaScript code that simulates clicking a "Load More" button and waits for new content to load. + +3. **Wait Condition**: The `wait_for` function ensures that the page has loaded more than 10 articles before proceeding with the extraction. + +4. **CSS Selector**: The `css_selector="article.tease-card"` parameter ensures that only article cards are extracted from the web page. + +5. **Extraction Strategy**: The `CosineStrategy` is used with a semantic filter for "technology" to extract relevant content based on cosine similarity. + +6. **Chunking Strategy**: We use `RegexChunking()` to split the content into manageable chunks for processing. + +## Advanced Usage: Custom Session and Multiple Requests + +For more complex scenarios where you need to maintain state across multiple requests or execute additional JavaScript after the initial page load, you can use a custom session: + +```python +async def advanced_crawl(): + async with AsyncWebCrawler(verbose=True) as crawler: + # Initial crawl with custom session + result1 = await crawler.arun( + url="https://www.nbcnews.com/business", + js_code=js_code, + wait_for=wait_for, + css_selector="article.tease-card", + session_id="business_session" + ) + + # Execute additional JavaScript in the same session + result2 = await crawler.crawler_strategy.execute_js( + session_id="business_session", + js_code="window.scrollTo(0, document.body.scrollHeight);", + wait_for_js="() => window.innerHeight + window.scrollY >= document.body.offsetHeight" + ) + + # Process results + print("Initial crawl result:", result1.extracted_content) + print("Additional JS execution result:", result2.html) + +asyncio.run(advanced_crawl()) +``` + +This advanced example demonstrates how to: +1. Use a custom session to maintain state across requests. +2. Execute additional JavaScript after the initial page load. +3. Wait for specific conditions using JavaScript functions. ## Try It Yourself -This example demonstrates the power and flexibility of Crawl4AI in handling complex web interactions and extracting meaningful data. You can customize the JavaScript code, CSS selectors, and extraction strategies to suit your specific requirements. +These examples demonstrate the power and flexibility of Crawl4AI's AsyncWebCrawler in handling complex web interactions and extracting meaningful data asynchronously. You can customize the JavaScript code, CSS selectors, extraction strategies, and waiting conditions to suit your specific requirements. \ No newline at end of file diff --git a/docs/md/examples/json_css_extraction.md b/docs/md/examples/json_css_extraction.md new file mode 100644 index 00000000..933d1f68 --- /dev/null +++ b/docs/md/examples/json_css_extraction.md @@ -0,0 +1,142 @@ +# JSON CSS Extraction Strategy with AsyncWebCrawler + +The `JsonCssExtractionStrategy` is a powerful feature of Crawl4AI that allows you to extract structured data from web pages using CSS selectors. This method is particularly useful when you need to extract specific data points from a consistent HTML structure, such as tables or repeated elements. Here's how to use it with the AsyncWebCrawler. + +## Overview + +The `JsonCssExtractionStrategy` works by defining a schema that specifies: +1. A base CSS selector for the repeating elements +2. Fields to extract from each element, each with its own CSS selector + +This strategy is fast and efficient, as it doesn't rely on external services like LLMs for extraction. + +## Example: Extracting Cryptocurrency Prices from Coinbase + +Let's look at an example that extracts cryptocurrency prices from the Coinbase explore page. + +```python +import json +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +async def extract_structured_data_using_css_extractor(): + print("\n--- Using JsonCssExtractionStrategy for Fast Structured Output ---") + + # Define the extraction schema + schema = { + "name": "Coinbase Crypto Prices", + "baseSelector": ".cds-tableRow-t45thuk", + "fields": [ + { + "name": "crypto", + "selector": "td:nth-child(1) h2", + "type": "text", + }, + { + "name": "symbol", + "selector": "td:nth-child(1) p", + "type": "text", + }, + { + "name": "price", + "selector": "td:nth-child(2)", + "type": "text", + } + ], + } + + # Create the extraction strategy + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + # Use the AsyncWebCrawler with the extraction strategy + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.coinbase.com/explore", + extraction_strategy=extraction_strategy, + bypass_cache=True, + ) + + assert result.success, "Failed to crawl the page" + + # Parse the extracted content + crypto_prices = json.loads(result.extracted_content) + print(f"Successfully extracted {len(crypto_prices)} cryptocurrency prices") + print(json.dumps(crypto_prices[0], indent=2)) + + return crypto_prices + +# Run the async function +asyncio.run(extract_structured_data_using_css_extractor()) +``` + +## Explanation of the Schema + +The schema defines how to extract the data: + +- `name`: A descriptive name for the extraction task. +- `baseSelector`: The CSS selector for the repeating elements (in this case, table rows). +- `fields`: An array of fields to extract from each element: + - `name`: The name to give the extracted data. + - `selector`: The CSS selector to find the specific data within the base element. + - `type`: The type of data to extract (usually "text" for textual content). + +## Advantages of JsonCssExtractionStrategy + +1. **Speed**: CSS selectors are fast to execute, making this method efficient for large datasets. +2. **Precision**: You can target exactly the elements you need. +3. **Structured Output**: The result is already structured as JSON, ready for further processing. +4. **No External Dependencies**: Unlike LLM-based strategies, this doesn't require any API calls to external services. + +## Tips for Using JsonCssExtractionStrategy + +1. **Inspect the Page**: Use browser developer tools to identify the correct CSS selectors. +2. **Test Selectors**: Verify your selectors in the browser console before using them in the script. +3. **Handle Dynamic Content**: If the page uses JavaScript to load content, you may need to combine this with JS execution (see the Advanced Usage section). +4. **Error Handling**: Always check the `result.success` flag and handle potential failures. + +## Advanced Usage: Combining with JavaScript Execution + +For pages that load data dynamically, you can combine the `JsonCssExtractionStrategy` with JavaScript execution: + +```python +async def extract_dynamic_structured_data(): + schema = { + "name": "Dynamic Crypto Prices", + "baseSelector": ".crypto-row", + "fields": [ + {"name": "name", "selector": ".crypto-name", "type": "text"}, + {"name": "price", "selector": ".crypto-price", "type": "text"}, + ] + } + + js_code = """ + window.scrollTo(0, document.body.scrollHeight); + await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for 2 seconds + """ + + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://example.com/crypto-prices", + extraction_strategy=extraction_strategy, + js_code=js_code, + wait_for=".crypto-row:nth-child(20)", # Wait for 20 rows to load + bypass_cache=True, + ) + + crypto_data = json.loads(result.extracted_content) + print(f"Extracted {len(crypto_data)} cryptocurrency entries") + +asyncio.run(extract_dynamic_structured_data()) +``` + +This advanced example demonstrates how to: +1. Execute JavaScript to trigger dynamic content loading. +2. Wait for a specific condition (20 rows loaded) before extraction. +3. Extract data from the dynamically loaded content. + +By mastering the `JsonCssExtractionStrategy`, you can efficiently extract structured data from a wide variety of web pages, making it a valuable tool in your web scraping toolkit. + +For more details on schema definitions and advanced extraction strategies, check out the[Advanced JsonCssExtraction](../full_details/advanced_jsoncss_extraction.md). \ No newline at end of file diff --git a/docs/md/examples/llm_extraction.md b/docs/md/examples/llm_extraction.md index b7805726..f4bd74c8 100644 --- a/docs/md/examples/llm_extraction.md +++ b/docs/md/examples/llm_extraction.md @@ -1,6 +1,6 @@ -# LLM Extraction +# LLM Extraction with AsyncWebCrawler -Crawl4AI allows you to use Language Models (LLMs) to extract structured data or relevant content from web pages. Below are two examples demonstrating how to use LLMExtractionStrategy for different purposes. +Crawl4AI's AsyncWebCrawler allows you to use Language Models (LLMs) to extract structured data or relevant content from web pages asynchronously. Below are two examples demonstrating how to use `LLMExtractionStrategy` for different purposes with the AsyncWebCrawler. ## Example 1: Extract Structured Data @@ -8,17 +8,10 @@ In this example, we use the `LLMExtractionStrategy` to extract structured data ( ```python import os -import time -from crawl4ai.web_crawler import WebCrawler -from crawl4ai.chunking_strategy import * -from crawl4ai.extraction_strategy import * -from crawl4ai.crawler_strategy import * - -url = r'https://openai.com/api/pricing/' - -crawler = WebCrawler() -crawler.warmup() - +import json +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy from pydantic import BaseModel, Field class OpenAIModelFee(BaseModel): @@ -26,27 +19,33 @@ class OpenAIModelFee(BaseModel): input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") -result = crawler.run( - url=url, - word_count_threshold=1, - extraction_strategy= LLMExtractionStrategy( - provider= "openai/gpt-4o", api_token = os.getenv('OPENAI_API_KEY'), - schema=OpenAIModelFee.model_json_schema(), - extraction_type="schema", - instruction="From the crawled content, extract all mentioned model names along with their "\ - "fees for input and output tokens. Make sure not to miss anything in the entire content. "\ - 'One extracted model JSON format should look like this: '\ - '{ "model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens" }' - ), - bypass_cache=True, -) +async def extract_openai_fees(): + url = 'https://openai.com/api/pricing/' -model_fees = json.loads(result.extracted_content) + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url=url, + word_count_threshold=1, + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + schema=OpenAIModelFee.model_json_schema(), + extraction_type="schema", + instruction="From the crawled content, extract all mentioned model names along with their " + "fees for input and output tokens. Make sure not to miss anything in the entire content. " + 'One extracted model JSON format should look like this: ' + '{ "model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens" }' + ), + bypass_cache=True, + ) -print(len(model_fees)) + model_fees = json.loads(result.extracted_content) + print(f"Number of models extracted: {len(model_fees)}") -with open(".data/data.json", "w", encoding="utf-8") as f: - f.write(result.extracted_content) + with open(".data/openai_fees.json", "w", encoding="utf-8") as f: + json.dump(model_fees, f, indent=2) + +asyncio.run(extract_openai_fees()) ``` ## Example 2: Extract Relevant Content @@ -54,30 +53,80 @@ with open(".data/data.json", "w", encoding="utf-8") as f: In this example, we instruct the LLM to extract only content related to technology from the NBC News business page. ```python -crawler = WebCrawler() -crawler.warmup() +import os +import json +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy -result = crawler.run( - url="https://www.nbcnews.com/business", - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - instruction="Extract only content related to technology" - ), - bypass_cache=True, - ) +async def extract_tech_content(): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.nbcnews.com/business", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Extract only content related to technology" + ), + bypass_cache=True, + ) -model_fees = json.loads(result.extracted_content) + tech_content = json.loads(result.extracted_content) + print(f"Number of tech-related items extracted: {len(tech_content)}") -print(len(model_fees)) + with open(".data/tech_content.json", "w", encoding="utf-8") as f: + json.dump(tech_content, f, indent=2) -with open(".data/data.json", "w", encoding="utf-8") as f: - f.write(result.extracted_content) +asyncio.run(extract_tech_content()) +``` + +## Advanced Usage: Combining JS Execution with LLM Extraction + +This example demonstrates how to combine JavaScript execution with LLM extraction to handle dynamic content: + +```python +async def extract_dynamic_content(): + js_code = """ + const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); + if (loadMoreButton) { + loadMoreButton.click(); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + """ + + wait_for = """ + () => { + const articles = document.querySelectorAll('article.tease-card'); + return articles.length > 10; + } + """ + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.nbcnews.com/business", + js_code=js_code, + wait_for=wait_for, + css_selector="article.tease-card", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Summarize each article, focusing on technology-related content" + ), + bypass_cache=True, + ) + + summaries = json.loads(result.extracted_content) + print(f"Number of summarized articles: {len(summaries)}") + + with open(".data/tech_summaries.json", "w", encoding="utf-8") as f: + json.dump(summaries, f, indent=2) + +asyncio.run(extract_dynamic_content()) ``` ## Customizing LLM Provider -Under the hood, Crawl4AI uses the `litellm` library, which allows you to use any LLM provider you want. Just pass the correct model name and API token. +Crawl4AI uses the `litellm` library under the hood, which allows you to use any LLM provider you want. Just pass the correct model name and API token: ```python extraction_strategy=LLMExtractionStrategy( @@ -88,3 +137,43 @@ extraction_strategy=LLMExtractionStrategy( ``` This flexibility allows you to integrate with various LLM providers and tailor the extraction process to your specific needs. + +## Error Handling and Retries + +When working with external LLM APIs, it's important to handle potential errors and implement retry logic. Here's an example of how you might do this: + +```python +import asyncio +from tenacity import retry, stop_after_attempt, wait_exponential + +class LLMExtractionError(Exception): + pass + +@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) +async def extract_with_retry(crawler, url, extraction_strategy): + try: + result = await crawler.arun(url=url, extraction_strategy=extraction_strategy, bypass_cache=True) + return json.loads(result.extracted_content) + except Exception as e: + raise LLMExtractionError(f"Failed to extract content: {str(e)}") + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + try: + content = await extract_with_retry( + crawler, + "https://www.example.com", + LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Extract and summarize main points" + ) + ) + print("Extracted content:", content) + except LLMExtractionError as e: + print(f"Extraction failed after retries: {e}") + +asyncio.run(main()) +``` + +This example uses the `tenacity` library to implement a retry mechanism with exponential backoff, which can help handle temporary failures or rate limiting from the LLM API. \ No newline at end of file diff --git a/docs/md/examples/research_assistant.md b/docs/md/examples/research_assistant.md index 9284fbef..61886f9e 100644 --- a/docs/md/examples/research_assistant.md +++ b/docs/md/examples/research_assistant.md @@ -1,33 +1,32 @@ -## Research Assistant Example +# Research Assistant Example with AsyncWebCrawler -This example demonstrates how to build a research assistant using `Chainlit` and `Crawl4AI`. The assistant will be capable of crawling web pages for information and answering questions based on the crawled content. Additionally, it integrates speech-to-text functionality for audio inputs. +This example demonstrates how to build an advanced research assistant using `Chainlit`, `Crawl4AI`'s `AsyncWebCrawler`, and various AI services. The assistant can crawl web pages asynchronously, answer questions based on the crawled content, and handle audio inputs. -### Step-by-Step Guide +## Step-by-Step Guide 1. **Install Required Packages** - Ensure you have the necessary packages installed. You need `chainlit`, `groq`, `requests`, and `openai`. + Ensure you have the necessary packages installed: ```bash - pip install chainlit groq requests openai + pip install chainlit groq openai crawl4ai ``` 2. **Import Libraries** - Import all the necessary modules and initialize the OpenAI client. - ```python import os import time + import asyncio from openai import AsyncOpenAI import chainlit as cl import re - import requests from io import BytesIO from chainlit.element import ElementBased from groq import Groq - - from concurrent.futures import ThreadPoolExecutor + from crawl4ai import AsyncWebCrawler + from crawl4ai.extraction_strategy import NoExtractionStrategy + from crawl4ai.chunking_strategy import RegexChunking client = AsyncOpenAI(base_url="https://api.groq.com/openai/v1", api_key=os.getenv("GROQ_API_KEY")) @@ -37,8 +36,6 @@ This example demonstrates how to build a research assistant using `Chainlit` and 3. **Set Configuration** - Define the model settings for the assistant. - ```python settings = { "model": "llama3-8b-8192", @@ -52,35 +49,25 @@ This example demonstrates how to build a research assistant using `Chainlit` and 4. **Define Utility Functions** - - **Extract URLs from Text**: Use regex to find URLs in messages. + ```python + def extract_urls(text): + url_pattern = re.compile(r'(https?://\S+)') + return url_pattern.findall(text) - ```python - def extract_urls(text): - url_pattern = re.compile(r'(https?://\S+)') - return url_pattern.findall(text) - ``` - - - **Crawl URL**: Send a request to `Crawl4AI` to fetch the content of a URL. - - ```python - def crawl_url(url): - data = { - "urls": [url], - "include_raw_html": True, - "word_count_threshold": 10, - "extraction_strategy": "NoExtractionStrategy", - "chunking_strategy": "RegexChunking" - } - response = requests.post("https://crawl4ai.com/crawl", json=data) - response_data = response.json() - response_data = response_data['results'][0] - return response_data['markdown'] - ``` + async def crawl_urls(urls): + async with AsyncWebCrawler(verbose=True) as crawler: + results = await crawler.arun_many( + urls=urls, + word_count_threshold=10, + extraction_strategy=NoExtractionStrategy(), + chunking_strategy=RegexChunking(), + bypass_cache=True + ) + return [result.markdown for result in results if result.success] + ``` 5. **Initialize Chat Start Event** - Set up the initial chat message and user session. - ```python @cl.on_chat_start async def on_chat_start(): @@ -88,15 +75,11 @@ This example demonstrates how to build a research assistant using `Chainlit` and "history": [], "context": {} }) - await cl.Message( - content="Welcome to the chat! How can I assist you today?" - ).send() + await cl.Message(content="Welcome to the chat! How can I assist you today?").send() ``` 6. **Handle Incoming Messages** - Process user messages, extract URLs, and crawl them concurrently. Update the chat history and system message. - ```python @cl.on_message async def on_message(message: cl.Message): @@ -105,19 +88,14 @@ This example demonstrates how to build a research assistant using `Chainlit` and # Extract URLs from the user's message urls = extract_urls(message.content) - futures = [] - with ThreadPoolExecutor() as executor: - for url in urls: - futures.append(executor.submit(crawl_url, url)) - - results = [future.result() for future in futures] - - for url, result in zip(urls, results): - ref_number = f"REF_{len(user_session['context']) + 1}" - user_session["context"][ref_number] = { - "url": url, - "content": result - } + if urls: + crawled_contents = await crawl_urls(urls) + for url, content in zip(urls, crawled_contents): + ref_number = f"REF_{len(user_session['context']) + 1}" + user_session["context"][ref_number] = { + "url": url, + "content": content + } user_session["history"].append({ "role": "user", @@ -129,33 +107,24 @@ This example demonstrates how to build a research assistant using `Chainlit` and f'\n{data["content"]}\n' for ref, data in user_session["context"].items() ] - if context_messages: - system_message = { - "role": "system", - "content": ( - "You are a helpful bot. Use the following context for answering questions. " - "Refer to the sources using the REF number in square brackets, e.g., [1], only if the source is given in the appendices below.\n\n" - "If the question requires any information from the provided appendices or context, refer to the sources. " - "If not, there is no need to add a references section. " - "At the end of your response, provide a reference section listing the URLs and their REF numbers only if sources from the appendices were used.\n\n" - "\n\n".join(context_messages) - ) - } - else: - system_message = { - "role": "system", - "content": "You are a helpful assistant." - } + system_message = { + "role": "system", + "content": ( + "You are a helpful bot. Use the following context for answering questions. " + "Refer to the sources using the REF number in square brackets, e.g., [1], only if the source is given in the appendices below.\n\n" + "If the question requires any information from the provided appendices or context, refer to the sources. " + "If not, there is no need to add a references section. " + "At the end of your response, provide a reference section listing the URLs and their REF numbers only if sources from the appendices were used.\n\n" + "\n\n".join(context_messages) + ) if context_messages else "You are a helpful assistant." + } msg = cl.Message(content="") await msg.send() # Get response from the LLM stream = await client.chat.completions.create( - messages=[ - system_message, - *user_session["history"] - ], + messages=[system_message, *user_session["history"]], stream=True, **settings ) @@ -174,18 +143,16 @@ This example demonstrates how to build a research assistant using `Chainlit` and await msg.update() # Append the reference section to the assistant's response - reference_section = "\n\nReferences:\n" - for ref, data in user_session["context"].items(): - reference_section += f"[{ref.split('_')[1]}]: {data['url']}\n" - - msg.content += reference_section - await msg.update() + if user_session["context"]: + reference_section = "\n\nReferences:\n" + for ref, data in user_session["context"].items(): + reference_section += f"[{ref.split('_')[1]}]: {data['url']}\n" + msg.content += reference_section + await msg.update() ``` 7. **Handle Audio Input** - Capture and transcribe audio input. Store the audio buffer and transcribe it when the audio ends. - ```python @cl.on_audio_chunk async def on_audio_chunk(chunk: cl.AudioChunk): @@ -194,12 +161,10 @@ This example demonstrates how to build a research assistant using `Chainlit` and buffer.name = f"input_audio.{chunk.mimeType.split('/')[1]}" cl.user_session.set("audio_buffer", buffer) cl.user_session.set("audio_mime_type", chunk.mimeType) - cl.user_session.get("audio_buffer").write(chunk.data) @cl.step(type="tool") async def speech_to_text(audio_file): - cli = Groq() response = await client.audio.transcriptions.create( model="whisper-large-v3", file=audio_file ) @@ -217,32 +182,39 @@ This example demonstrates how to build a research assistant using `Chainlit` and end_time = time.time() print(f"Transcription took {end_time - start_time} seconds") - user_msg = cl.Message( - author="You", - type="user_message", - content=transcription - ) + user_msg = cl.Message(author="You", type="user_message", content=transcription) await user_msg.send() await on_message(user_msg) ``` 8. **Run the Chat Application** - Start the Chainlit application. - ```python if __name__ == "__main__": from chainlit.cli import run_chainlit run_chainlit(__file__) ``` -### Explanation +## Explanation -- **Libraries and Configuration**: Import necessary libraries and configure the OpenAI client. -- **Utility Functions**: Define functions to extract URLs and crawl them. -- **Chat Start Event**: Initialize chat session and welcome message. -- **Message Handling**: Extract URLs, crawl them concurrently, and update chat history and context. -- **Audio Handling**: Capture, buffer, and transcribe audio input, then process the transcription as text. -- **Running the Application**: Start the Chainlit server to interact with the assistant. +- **Libraries and Configuration**: We import necessary libraries, including `AsyncWebCrawler` from `crawl4ai`. +- **Utility Functions**: + - `extract_urls`: Uses regex to find URLs in messages. + - `crawl_urls`: An asynchronous function that uses `AsyncWebCrawler` to fetch content from multiple URLs concurrently. +- **Chat Start Event**: Initializes the chat session and sends a welcome message. +- **Message Handling**: + - Extracts URLs from user messages. + - Asynchronously crawls the URLs using `AsyncWebCrawler`. + - Updates chat history and context with crawled content. + - Generates a response using the LLM, incorporating the crawled context. +- **Audio Handling**: Captures, buffers, and transcribes audio input, then processes the transcription as text. +- **Running the Application**: Starts the Chainlit server for interaction with the assistant. -This example showcases how to create an interactive research assistant that can fetch, process, and summarize web content, along with handling audio inputs for a seamless user experience. +## Key Improvements + +1. **Asynchronous Web Crawling**: Using `AsyncWebCrawler` allows for efficient, concurrent crawling of multiple URLs. +2. **Improved Context Management**: The assistant now maintains a context of crawled content, allowing for more informed responses. +3. **Dynamic Reference System**: The assistant can refer to specific sources in its responses and provide a reference section. +4. **Seamless Audio Integration**: The ability to handle audio inputs makes the assistant more versatile and user-friendly. + +This updated Research Assistant showcases how to create a powerful, interactive tool that can efficiently fetch and process web content, handle various input types, and provide informed responses based on the gathered information. \ No newline at end of file diff --git a/docs/md/examples/summarization.md b/docs/md/examples/summarization.md index b817f691..12c18471 100644 --- a/docs/md/examples/summarization.md +++ b/docs/md/examples/summarization.md @@ -1,44 +1,34 @@ -## Summarization Example +# Summarization Example with AsyncWebCrawler -This example demonstrates how to use `Crawl4AI` to extract a summary from a web page. The goal is to obtain the title, a detailed summary, a brief summary, and a list of keywords from the given page. +This example demonstrates how to use Crawl4AI's `AsyncWebCrawler` to extract a summary from a web page asynchronously. The goal is to obtain the title, a detailed summary, a brief summary, and a list of keywords from the given page. -### Step-by-Step Guide +## Step-by-Step Guide 1. **Import Necessary Modules** - First, import the necessary modules and classes. + First, import the necessary modules and classes: ```python import os - import time import json - from crawl4ai.web_crawler import WebCrawler - from crawl4ai.chunking_strategy import * - from crawl4ai.extraction_strategy import * - from crawl4ai.crawler_strategy import * + import asyncio + from crawl4ai import AsyncWebCrawler + from crawl4ai.extraction_strategy import LLMExtractionStrategy + from crawl4ai.chunking_strategy import RegexChunking from pydantic import BaseModel, Field ``` 2. **Define the URL to be Crawled** - Set the URL of the web page you want to summarize. + Set the URL of the web page you want to summarize: ```python - url = r'https://marketplace.visualstudio.com/items?itemName=Unclecode.groqopilot' + url = 'https://marketplace.visualstudio.com/items?itemName=Unclecode.groqopilot' ``` -3. **Initialize the WebCrawler** +3. **Define the Data Model** - Create an instance of the `WebCrawler` and call the `warmup` method. - - ```python - crawler = WebCrawler() - crawler.warmup() - ``` - -4. **Define the Data Model** - - Use Pydantic to define the structure of the extracted data. + Use Pydantic to define the structure of the extracted data: ```python class PageSummary(BaseModel): @@ -48,61 +38,116 @@ This example demonstrates how to use `Crawl4AI` to extract a summary from a web keywords: list = Field(..., description="Keywords assigned to the page.") ``` -5. **Run the Crawler** +4. **Create the Extraction Strategy** - Set up and run the crawler with the `LLMExtractionStrategy`. Provide the necessary parameters, including the schema for the extracted data and the instruction for the LLM. + Set up the `LLMExtractionStrategy` with the necessary parameters: ```python - result = crawler.run( - url=url, - word_count_threshold=1, - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - schema=PageSummary.model_json_schema(), - extraction_type="schema", - apply_chunking=False, - instruction=( - "From the crawled content, extract the following details: " - "1. Title of the page " - "2. Summary of the page, which is a detailed summary " - "3. Brief summary of the page, which is a paragraph text " - "4. Keywords assigned to the page, which is a list of keywords. " - 'The extracted JSON format should look like this: ' - '{ "title": "Page Title", "summary": "Detailed summary of the page.", ' - '"brief_summary": "Brief summary in a paragraph.", "keywords": ["keyword1", "keyword2", "keyword3"] }' - ) - ), - bypass_cache=True, + extraction_strategy = LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + schema=PageSummary.model_json_schema(), + extraction_type="schema", + apply_chunking=False, + instruction=( + "From the crawled content, extract the following details: " + "1. Title of the page " + "2. Summary of the page, which is a detailed summary " + "3. Brief summary of the page, which is a paragraph text " + "4. Keywords assigned to the page, which is a list of keywords. " + 'The extracted JSON format should look like this: ' + '{ "title": "Page Title", "summary": "Detailed summary of the page.", ' + '"brief_summary": "Brief summary in a paragraph.", "keywords": ["keyword1", "keyword2", "keyword3"] }' + ) ) ``` -6. **Process the Extracted Data** +5. **Define the Async Crawl Function** - Load the extracted content into a JSON object and print it. + Create an asynchronous function to run the crawler: ```python - page_summary = json.loads(result.extracted_content) - print(page_summary) + async def crawl_and_summarize(url): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url=url, + word_count_threshold=1, + extraction_strategy=extraction_strategy, + chunking_strategy=RegexChunking(), + bypass_cache=True, + ) + return result ``` -7. **Save the Extracted Data** +6. **Run the Crawler and Process Results** - Save the extracted data to a file for further use. + Use asyncio to run the crawler and process the results: ```python - with open(".data/page_summary.json", "w", encoding="utf-8") as f: - f.write(result.extracted_content) + async def main(): + result = await crawl_and_summarize(url) + + if result.success: + page_summary = json.loads(result.extracted_content) + print("Extracted Page Summary:") + print(json.dumps(page_summary, indent=2)) + + # Save the extracted data + with open(".data/page_summary.json", "w", encoding="utf-8") as f: + json.dump(page_summary, f, indent=2) + print("Page summary saved to .data/page_summary.json") + else: + print(f"Failed to crawl and summarize the page. Error: {result.error_message}") + + # Run the async main function + asyncio.run(main()) ``` -### Explanation +## Explanation -- **Importing Modules**: Import the necessary modules, including `WebCrawler` and `LLMExtractionStrategy` from `Crawl4AI`. -- **URL Definition**: Set the URL of the web page you want to crawl and summarize. -- **WebCrawler Initialization**: Create an instance of `WebCrawler` and call the `warmup` method to prepare the crawler. -- **Data Model Definition**: Define the structure of the data you want to extract using Pydantic's `BaseModel`. -- **Crawler Execution**: Run the crawler with the `LLMExtractionStrategy`, providing the schema and detailed instructions for the extraction process. -- **Data Processing**: Load the extracted content into a JSON object and print it to verify the results. -- **Data Saving**: Save the extracted data to a file for further use. +- **Importing Modules**: We import the necessary modules, including `AsyncWebCrawler` and `LLMExtractionStrategy` from Crawl4AI. +- **URL Definition**: We set the URL of the web page to crawl and summarize. +- **Data Model Definition**: We define the structure of the data to extract using Pydantic's `BaseModel`. +- **Extraction Strategy Setup**: We create an instance of `LLMExtractionStrategy` with the schema and detailed instructions for the extraction process. +- **Async Crawl Function**: We define an asynchronous function `crawl_and_summarize` that uses `AsyncWebCrawler` to perform the crawling and extraction. +- **Main Execution**: In the `main` function, we run the crawler, process the results, and save the extracted data. -This example demonstrates how to harness the power of `Crawl4AI` to perform advanced web crawling and data extraction tasks with minimal code. +## Advanced Usage: Crawling Multiple URLs + +To demonstrate the power of `AsyncWebCrawler`, here's how you can summarize multiple pages concurrently: + +```python +async def crawl_multiple_urls(urls): + async with AsyncWebCrawler(verbose=True) as crawler: + tasks = [crawler.arun( + url=url, + word_count_threshold=1, + extraction_strategy=extraction_strategy, + chunking_strategy=RegexChunking(), + bypass_cache=True + ) for url in urls] + results = await asyncio.gather(*tasks) + return results + +async def main(): + urls = [ + 'https://marketplace.visualstudio.com/items?itemName=Unclecode.groqopilot', + 'https://marketplace.visualstudio.com/items?itemName=GitHub.copilot', + 'https://marketplace.visualstudio.com/items?itemName=ms-python.python' + ] + results = await crawl_multiple_urls(urls) + + for i, result in enumerate(results): + if result.success: + page_summary = json.loads(result.extracted_content) + print(f"\nSummary for URL {i+1}:") + print(json.dumps(page_summary, indent=2)) + else: + print(f"\nFailed to summarize URL {i+1}. Error: {result.error_message}") + +asyncio.run(main()) +``` + +This advanced example shows how to use `AsyncWebCrawler` to efficiently summarize multiple web pages concurrently, significantly reducing the total processing time compared to sequential crawling. + +By leveraging the asynchronous capabilities of Crawl4AI, you can perform advanced web crawling and data extraction tasks with improved efficiency and scalability. \ No newline at end of file diff --git a/docs/md/full_details/advanced_jsoncss_extraction.md b/docs/md/full_details/advanced_jsoncss_extraction.md new file mode 100644 index 00000000..393b79a5 --- /dev/null +++ b/docs/md/full_details/advanced_jsoncss_extraction.md @@ -0,0 +1,282 @@ +# Advanced Usage of JsonCssExtractionStrategy + +While the basic usage of JsonCssExtractionStrategy is powerful for simple structures, its true potential shines when dealing with complex, nested HTML structures. This section will explore advanced usage scenarios, demonstrating how to extract nested objects, lists, and nested lists. + +## Hypothetical Website Example + +Let's consider a hypothetical e-commerce website that displays product categories, each containing multiple products. Each product has details, reviews, and related items. This complex structure will allow us to demonstrate various advanced features of JsonCssExtractionStrategy. + +Assume the HTML structure looks something like this: + +```html +

+

Electronics

+
+

Smartphone X

+

$999

+
+ TechCorp + X-2000 +
+
    +
  • 5G capable
  • +
  • 6.5" OLED screen
  • +
  • 128GB storage
  • +
+
+
+ John D. + 4.5 +

Great phone, love the camera!

+
+
+ Jane S. + 5 +

Best smartphone I've ever owned.

+
+
+ +
+ +
+``` + +Now, let's create a schema to extract this complex structure: + +```python +schema = { + "name": "E-commerce Product Catalog", + "baseSelector": "div.category", + "fields": [ + { + "name": "category_name", + "selector": "h2.category-name", + "type": "text" + }, + { + "name": "products", + "selector": "div.product", + "type": "nested_list", + "fields": [ + { + "name": "name", + "selector": "h3.product-name", + "type": "text" + }, + { + "name": "price", + "selector": "p.product-price", + "type": "text" + }, + { + "name": "details", + "selector": "div.product-details", + "type": "nested", + "fields": [ + { + "name": "brand", + "selector": "span.brand", + "type": "text" + }, + { + "name": "model", + "selector": "span.model", + "type": "text" + } + ] + }, + { + "name": "features", + "selector": "ul.product-features li", + "type": "list", + "fields": [ + { + "name": "feature", + "type": "text" + } + ] + }, + { + "name": "reviews", + "selector": "div.review", + "type": "nested_list", + "fields": [ + { + "name": "reviewer", + "selector": "span.reviewer", + "type": "text" + }, + { + "name": "rating", + "selector": "span.rating", + "type": "text" + }, + { + "name": "comment", + "selector": "p.review-text", + "type": "text" + } + ] + }, + { + "name": "related_products", + "selector": "ul.related-products li", + "type": "list", + "fields": [ + { + "name": "name", + "selector": "span.related-name", + "type": "text" + }, + { + "name": "price", + "selector": "span.related-price", + "type": "text" + } + ] + } + ] + } + ] +} +``` + +This schema demonstrates several advanced features: + +1. **Nested Objects**: The `details` field is a nested object within each product. +2. **Simple Lists**: The `features` field is a simple list of text items. +3. **Nested Lists**: The `products` field is a nested list, where each item is a complex object. +4. **Lists of Objects**: The `reviews` and `related_products` fields are lists of objects. + +Let's break down the key concepts: + +### Nested Objects + +To create a nested object, use `"type": "nested"` and provide a `fields` array for the nested structure: + +```python +{ + "name": "details", + "selector": "div.product-details", + "type": "nested", + "fields": [ + { + "name": "brand", + "selector": "span.brand", + "type": "text" + }, + { + "name": "model", + "selector": "span.model", + "type": "text" + } + ] +} +``` + +### Simple Lists + +For a simple list of identical items, use `"type": "list"`: + +```python +{ + "name": "features", + "selector": "ul.product-features li", + "type": "list", + "fields": [ + { + "name": "feature", + "type": "text" + } + ] +} +``` + +### Nested Lists + +For a list of complex objects, use `"type": "nested_list"`: + +```python +{ + "name": "products", + "selector": "div.product", + "type": "nested_list", + "fields": [ + // ... fields for each product + ] +} +``` + +### Lists of Objects + +Similar to nested lists, but typically used for simpler objects within the list: + +```python +{ + "name": "related_products", + "selector": "ul.related-products li", + "type": "list", + "fields": [ + { + "name": "name", + "selector": "span.related-name", + "type": "text" + }, + { + "name": "price", + "selector": "span.related-price", + "type": "text" + } + ] +} +``` + +## Using the Advanced Schema + +To use this advanced schema with AsyncWebCrawler: + +```python +import json +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +async def extract_complex_product_data(): + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://gist.githubusercontent.com/githubusercontent/2d7b8ba3cd8ab6cf3c8da771ddb36878/raw/1ae2f90c6861ce7dd84cc50d3df9920dee5e1fd2/sample_ecommerce.html", + extraction_strategy=extraction_strategy, + bypass_cache=True, + ) + + assert result.success, "Failed to crawl the page" + + product_data = json.loads(result.extracted_content) + print(json.dumps(product_data, indent=2)) + +asyncio.run(extract_complex_product_data()) +``` + +This will produce a structured JSON output that captures the complex hierarchy of the product catalog, including nested objects, lists, and nested lists. + +## Tips for Advanced Usage + +1. **Start Simple**: Begin with a basic schema and gradually add complexity. +2. **Test Incrementally**: Test each part of your schema separately before combining them. +3. **Use Chrome DevTools**: The Element Inspector is invaluable for identifying the correct selectors. +4. **Handle Missing Data**: Use the `default` key in your field definitions to handle cases where data might be missing. +5. **Leverage Transforms**: Use the `transform` key to clean or format extracted data (e.g., converting prices to numbers). +6. **Consider Performance**: Very complex schemas might slow down extraction. Balance complexity with performance needs. + +By mastering these advanced techniques, you can use JsonCssExtractionStrategy to extract highly structured data from even the most complex web pages, making it a powerful tool for web scraping and data analysis tasks. \ No newline at end of file diff --git a/docs/md/full_details/crawl_request_parameters.md b/docs/md/full_details/crawl_request_parameters.md index c32743c6..896097de 100644 --- a/docs/md/full_details/crawl_request_parameters.md +++ b/docs/md/full_details/crawl_request_parameters.md @@ -1,6 +1,6 @@ -# Crawl Request Parameters +# Crawl Request Parameters for AsyncWebCrawler -The `run` function in Crawl4AI is designed to be highly configurable, allowing you to customize the crawling and extraction process to suit your needs. Below are the parameters you can use with the `run` function, along with their descriptions, possible values, and examples. +The `arun` method in Crawl4AI's `AsyncWebCrawler` is designed to be highly configurable, allowing you to customize the crawling and extraction process to suit your needs. Below are the parameters you can use with the `arun` method, along with their descriptions, possible values, and examples. ## Parameters @@ -13,9 +13,9 @@ url = "https://www.nbcnews.com/business" ``` ### word_count_threshold (int) -**Description:** The minimum number of words a block must contain to be considered meaningful. The default value is `5`. +**Description:** The minimum number of words a block must contain to be considered meaningful. The default value is defined by `MIN_WORD_THRESHOLD`. **Required:** No -**Default Value:** `5` +**Default Value:** `MIN_WORD_THRESHOLD` **Example:** ```python word_count_threshold = 10 @@ -88,43 +88,92 @@ verbose = True Additional keyword arguments that can be passed to customize the crawling process further. Some notable options include: - **only_text (bool):** Whether to extract only text content, excluding HTML tags. Default is `False`. +- **session_id (str):** A unique identifier for the crawling session. This is useful for maintaining state across multiple requests. +- **js_code (str or list):** JavaScript code to be executed on the page before extraction. +- **wait_for (str):** A CSS selector or JavaScript function to wait for before considering the page load complete. **Example:** ```python -result = crawler.run( +result = await crawler.arun( url="https://www.nbcnews.com/business", css_selector="p", - only_text=True + only_text=True, + session_id="unique_session_123", + js_code="window.scrollTo(0, document.body.scrollHeight);", + wait_for="article.main-article" ) ``` ## Example Usage -Here's an example of how to use the `run` function with various parameters: +Here's an example of how to use the `arun` method with various parameters: ```python -from crawl4ai import WebCrawler +import asyncio +from crawl4ai import AsyncWebCrawler from crawl4ai.extraction_strategy import CosineStrategy from crawl4ai.chunking_strategy import NlpSentenceChunking -# Create the WebCrawler instance -crawler = WebCrawler() +async def main(): + # Create the AsyncWebCrawler instance + async with AsyncWebCrawler(verbose=True) as crawler: + # Run the crawler with custom parameters + result = await crawler.arun( + url="https://www.nbcnews.com/business", + word_count_threshold=10, + extraction_strategy=CosineStrategy(semantic_filter="finance"), + chunking_strategy=NlpSentenceChunking(), + bypass_cache=True, + css_selector="div.article-content", + screenshot=True, + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", + verbose=True, + only_text=True, + session_id="business_news_session", + js_code="window.scrollTo(0, document.body.scrollHeight);", + wait_for="footer" + ) -# Run the crawler with custom parameters -result = crawler.run( - url="https://www.nbcnews.com/business", - word_count_threshold=10, - extraction_strategy=CosineStrategy(semantic_filter="finance"), - chunking_strategy=NlpSentenceChunking(), - bypass_cache=True, - css_selector="div.article-content", - screenshot=True, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", - verbose=True, - only_text=True -) + print(result) -print(result) +# Run the async function +asyncio.run(main()) ``` -This example demonstrates how to configure various parameters to customize the crawling and extraction process using Crawl4AI. +This example demonstrates how to configure various parameters to customize the crawling and extraction process using the asynchronous version of Crawl4AI. + +## Additional Asynchronous Methods + +The `AsyncWebCrawler` class also provides other useful asynchronous methods: + +### arun_many +**Description:** Crawl multiple URLs concurrently. +**Example:** +```python +urls = ["https://example1.com", "https://example2.com", "https://example3.com"] +results = await crawler.arun_many(urls, word_count_threshold=10, bypass_cache=True) +``` + +### aclear_cache +**Description:** Clear the crawler's cache. +**Example:** +```python +await crawler.aclear_cache() +``` + +### aflush_cache +**Description:** Completely flush the crawler's cache. +**Example:** +```python +await crawler.aflush_cache() +``` + +### aget_cache_size +**Description:** Get the current size of the cache. +**Example:** +```python +cache_size = await crawler.aget_cache_size() +print(f"Current cache size: {cache_size}") +``` + +These asynchronous methods allow for efficient and flexible use of the AsyncWebCrawler in various scenarios. \ No newline at end of file diff --git a/docs/md/full_details/crawl_result_class.md b/docs/md/full_details/crawl_result_class.md index f66e16dc..e2c545fe 100644 --- a/docs/md/full_details/crawl_result_class.md +++ b/docs/md/full_details/crawl_result_class.md @@ -5,6 +5,9 @@ The `CrawlResult` class is the heart of Crawl4AI's output, encapsulating all the ## Class Definition ```python +from pydantic import BaseModel +from typing import Dict, List, Optional + class CrawlResult(BaseModel): url: str html: str @@ -17,6 +20,9 @@ class CrawlResult(BaseModel): extracted_content: Optional[str] = None metadata: Optional[dict] = None error_message: Optional[str] = None + session_id: Optional[str] = None + responser_headers: Optional[dict] = None + status_code: Optional[int] = None ``` ## Fields Explanation @@ -34,7 +40,7 @@ A flag indicating whether the crawling and extraction were successful. If any er The cleaned HTML content of the web page. This field holds the HTML after removing unwanted tags like ` - \ No newline at end of file diff --git a/docs/md_v0/examples/hooks_auth.md b/docs/md_v0/examples/hooks_auth.md deleted file mode 100644 index 2b4c2701..00000000 --- a/docs/md_v0/examples/hooks_auth.md +++ /dev/null @@ -1,100 +0,0 @@ -# Hooks & Auth - -Crawl4AI allows you to customize the behavior of the web crawler using hooks. Hooks are functions that are called at specific points in the crawling process, allowing you to modify the crawler's behavior or perform additional actions. This example demonstrates how to use various hooks to customize the crawling process. - -## Example: Using Crawler Hooks - -Let's see how we can customize the crawler using hooks! In this example, we'll: - -1. Maximize the browser window and log in to a website when the driver is created. -2. Add a custom header before fetching the URL. -3. Log the current URL after fetching it. -4. Log the length of the HTML before returning it. - -### Hook Definitions - -```python -from crawl4ai.web_crawler import WebCrawler -from crawl4ai.crawler_strategy import * - -def on_driver_created(driver): - print("[HOOK] on_driver_created") - # Example customization: maximize the window - driver.maximize_window() - - # Example customization: logging in to a hypothetical website - driver.get('https://example.com/login') - - from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.common.by import By - from selenium.webdriver.support import expected_conditions as EC - - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.NAME, 'username')) - ) - driver.find_element(By.NAME, 'username').send_keys('testuser') - driver.find_element(By.NAME, 'password').send_keys('password123') - driver.find_element(By.NAME, 'login').click() - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.ID, 'welcome')) - ) - # Add a custom cookie - driver.add_cookie({'name': 'test_cookie', 'value': 'cookie_value'}) - return driver - - -def before_get_url(driver): - print("[HOOK] before_get_url") - # Example customization: add a custom header - # Enable Network domain for sending headers - driver.execute_cdp_cmd('Network.enable', {}) - # Add a custom header - driver.execute_cdp_cmd('Network.setExtraHTTPHeaders', {'headers': {'X-Test-Header': 'test'}}) - return driver - -def after_get_url(driver): - print("[HOOK] after_get_url") - # Example customization: log the URL - print(driver.current_url) - return driver - -def before_return_html(driver, html): - print("[HOOK] before_return_html") - # Example customization: log the HTML - print(len(html)) - return driver -``` - -### Using the Hooks with the WebCrawler - -```python -print("\n🔗 [bold cyan]Using Crawler Hooks: Let's see how we can customize the crawler using hooks![/bold cyan]", True) -crawler_strategy = LocalSeleniumCrawlerStrategy(verbose=True) -crawler_strategy.set_hook('on_driver_created', on_driver_created) -crawler_strategy.set_hook('before_get_url', before_get_url) -crawler_strategy.set_hook('after_get_url', after_get_url) -crawler_strategy.set_hook('before_return_html', before_return_html) -crawler = WebCrawler(verbose=True, crawler_strategy=crawler_strategy) -crawler.warmup() - -result = crawler.run(url="https://example.com") - -print("[LOG] 📦 [bold yellow]Crawler Hooks result:[/bold yellow]") -print(result) -``` - -### Explanation - -- `on_driver_created`: This hook is called when the Selenium driver is created. In this example, it maximizes the window, logs in to a website, and adds a custom cookie. -- `before_get_url`: This hook is called right before Selenium fetches the URL. In this example, it adds a custom HTTP header. -- `after_get_url`: This hook is called after Selenium fetches the URL. In this example, it logs the current URL. -- `before_return_html`: This hook is called before returning the HTML content. In this example, it logs the length of the HTML content. - -### Additional Ideas - -- **Add custom headers to requests**: You can add custom headers to the requests using the `before_get_url` hook. -- **Perform safety checks**: Use the hooks to perform safety checks before the crawling process starts. -- **Modify the HTML content**: Use the `before_return_html` hook to modify the HTML content before it is returned. -- **Log additional information**: Use the hooks to log additional information for debugging or monitoring purposes. - -By using these hooks, you can customize the behavior of the crawler to suit your specific needs. diff --git a/docs/md_v0/examples/index.md b/docs/md_v0/examples/index.md deleted file mode 100644 index 14380e3d..00000000 --- a/docs/md_v0/examples/index.md +++ /dev/null @@ -1,29 +0,0 @@ -# Examples - -Welcome to the examples section of Crawl4AI documentation! In this section, you will find practical examples demonstrating how to use Crawl4AI for various web crawling and data extraction tasks. Each example is designed to showcase different features and capabilities of the library. - -## Examples Index - -### [LLM Extraction](llm_extraction.md) - -This example demonstrates how to use Crawl4AI to extract information using Large Language Models (LLMs). You will learn how to configure the `LLMExtractionStrategy` to get structured data from web pages. - -### [JS Execution & CSS Filtering](js_execution_css_filtering.md) - -Learn how to execute custom JavaScript code and filter data using CSS selectors. This example shows how to perform complex web interactions and extract specific content from web pages. - -### [Hooks & Auth](hooks_auth.md) - -This example covers the use of custom hooks for authentication and other pre-crawling tasks. You will see how to set up hooks to modify headers, authenticate sessions, and perform other preparatory actions before crawling. - -### [Summarization](summarization.md) - -Discover how to use Crawl4AI to summarize web page content. This example demonstrates the summarization capabilities of the library, helping you extract concise information from lengthy web pages. - -### [Research Assistant](research_assistant.md) - -In this example, Crawl4AI is used as a research assistant to gather and organize information from multiple sources. You will learn how to use various extraction and chunking strategies to compile a comprehensive report. - ---- - -Each example includes detailed explanations and code snippets to help you understand and implement the features in your projects. Click on the links to explore each example and start making the most of Crawl4AI! diff --git a/docs/md_v0/examples/js_execution_css_filtering.md b/docs/md_v0/examples/js_execution_css_filtering.md deleted file mode 100644 index 0592f094..00000000 --- a/docs/md_v0/examples/js_execution_css_filtering.md +++ /dev/null @@ -1,44 +0,0 @@ -# JS Execution & CSS Filtering - -In this example, we'll demonstrate how to use Crawl4AI to execute JavaScript, filter data with CSS selectors, and use a cosine similarity strategy to extract relevant content. This approach is particularly useful when you need to interact with dynamic content on web pages, such as clicking "Load More" buttons. - -## Example: Extracting Structured Data - -```python -# Import necessary modules -from crawl4ai import WebCrawler -from crawl4ai.chunking_strategy import * -from crawl4ai.extraction_strategy import * -from crawl4ai.crawler_strategy import * - -# Define the JavaScript code to click the "Load More" button -js_code = [""" -const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); -loadMoreButton && loadMoreButton.click(); -"""] - -crawler = WebCrawler(verbose=True) -crawler.warmup() -# Run the crawler with keyword filtering and CSS selector -result = crawler.run( - url="https://www.nbcnews.com/business", - js=js_code, - css_selector="p", - extraction_strategy=CosineStrategy( - semantic_filter="technology", - ), -) - -# Display the extracted result -print(result) -``` - -### Explanation - -1. **JavaScript Execution**: The `js_code` variable contains JavaScript code that simulates clicking a "Load More" button. This is useful for loading additional content dynamically. -2. **CSS Selector**: The `css_selector="p"` parameter ensures that only paragraph (`

`) tags are extracted from the web page. -3. **Extraction Strategy**: The `CosineStrategy` is used with a semantic filter for "technology" to extract relevant content based on cosine similarity. - -## Try It Yourself - -This example demonstrates the power and flexibility of Crawl4AI in handling complex web interactions and extracting meaningful data. You can customize the JavaScript code, CSS selectors, and extraction strategies to suit your specific requirements. diff --git a/docs/md_v0/examples/llm_extraction.md b/docs/md_v0/examples/llm_extraction.md deleted file mode 100644 index b7805726..00000000 --- a/docs/md_v0/examples/llm_extraction.md +++ /dev/null @@ -1,90 +0,0 @@ -# LLM Extraction - -Crawl4AI allows you to use Language Models (LLMs) to extract structured data or relevant content from web pages. Below are two examples demonstrating how to use LLMExtractionStrategy for different purposes. - -## Example 1: Extract Structured Data - -In this example, we use the `LLMExtractionStrategy` to extract structured data (model names and their fees) from the OpenAI pricing page. - -```python -import os -import time -from crawl4ai.web_crawler import WebCrawler -from crawl4ai.chunking_strategy import * -from crawl4ai.extraction_strategy import * -from crawl4ai.crawler_strategy import * - -url = r'https://openai.com/api/pricing/' - -crawler = WebCrawler() -crawler.warmup() - -from pydantic import BaseModel, Field - -class OpenAIModelFee(BaseModel): - model_name: str = Field(..., description="Name of the OpenAI model.") - input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") - output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") - -result = crawler.run( - url=url, - word_count_threshold=1, - extraction_strategy= LLMExtractionStrategy( - provider= "openai/gpt-4o", api_token = os.getenv('OPENAI_API_KEY'), - schema=OpenAIModelFee.model_json_schema(), - extraction_type="schema", - instruction="From the crawled content, extract all mentioned model names along with their "\ - "fees for input and output tokens. Make sure not to miss anything in the entire content. "\ - 'One extracted model JSON format should look like this: '\ - '{ "model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens" }' - ), - bypass_cache=True, -) - -model_fees = json.loads(result.extracted_content) - -print(len(model_fees)) - -with open(".data/data.json", "w", encoding="utf-8") as f: - f.write(result.extracted_content) -``` - -## Example 2: Extract Relevant Content - -In this example, we instruct the LLM to extract only content related to technology from the NBC News business page. - -```python -crawler = WebCrawler() -crawler.warmup() - -result = crawler.run( - url="https://www.nbcnews.com/business", - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - instruction="Extract only content related to technology" - ), - bypass_cache=True, - ) - -model_fees = json.loads(result.extracted_content) - -print(len(model_fees)) - -with open(".data/data.json", "w", encoding="utf-8") as f: - f.write(result.extracted_content) -``` - -## Customizing LLM Provider - -Under the hood, Crawl4AI uses the `litellm` library, which allows you to use any LLM provider you want. Just pass the correct model name and API token. - -```python -extraction_strategy=LLMExtractionStrategy( - provider="your_llm_provider/model_name", - api_token="your_api_token", - instruction="Your extraction instruction" -) -``` - -This flexibility allows you to integrate with various LLM providers and tailor the extraction process to your specific needs. diff --git a/docs/md_v0/examples/research_assistant.md b/docs/md_v0/examples/research_assistant.md deleted file mode 100644 index 9284fbef..00000000 --- a/docs/md_v0/examples/research_assistant.md +++ /dev/null @@ -1,248 +0,0 @@ -## Research Assistant Example - -This example demonstrates how to build a research assistant using `Chainlit` and `Crawl4AI`. The assistant will be capable of crawling web pages for information and answering questions based on the crawled content. Additionally, it integrates speech-to-text functionality for audio inputs. - -### Step-by-Step Guide - -1. **Install Required Packages** - - Ensure you have the necessary packages installed. You need `chainlit`, `groq`, `requests`, and `openai`. - - ```bash - pip install chainlit groq requests openai - ``` - -2. **Import Libraries** - - Import all the necessary modules and initialize the OpenAI client. - - ```python - import os - import time - from openai import AsyncOpenAI - import chainlit as cl - import re - import requests - from io import BytesIO - from chainlit.element import ElementBased - from groq import Groq - - from concurrent.futures import ThreadPoolExecutor - - client = AsyncOpenAI(base_url="https://api.groq.com/openai/v1", api_key=os.getenv("GROQ_API_KEY")) - - # Instrument the OpenAI client - cl.instrument_openai() - ``` - -3. **Set Configuration** - - Define the model settings for the assistant. - - ```python - settings = { - "model": "llama3-8b-8192", - "temperature": 0.5, - "max_tokens": 500, - "top_p": 1, - "frequency_penalty": 0, - "presence_penalty": 0, - } - ``` - -4. **Define Utility Functions** - - - **Extract URLs from Text**: Use regex to find URLs in messages. - - ```python - def extract_urls(text): - url_pattern = re.compile(r'(https?://\S+)') - return url_pattern.findall(text) - ``` - - - **Crawl URL**: Send a request to `Crawl4AI` to fetch the content of a URL. - - ```python - def crawl_url(url): - data = { - "urls": [url], - "include_raw_html": True, - "word_count_threshold": 10, - "extraction_strategy": "NoExtractionStrategy", - "chunking_strategy": "RegexChunking" - } - response = requests.post("https://crawl4ai.com/crawl", json=data) - response_data = response.json() - response_data = response_data['results'][0] - return response_data['markdown'] - ``` - -5. **Initialize Chat Start Event** - - Set up the initial chat message and user session. - - ```python - @cl.on_chat_start - async def on_chat_start(): - cl.user_session.set("session", { - "history": [], - "context": {} - }) - await cl.Message( - content="Welcome to the chat! How can I assist you today?" - ).send() - ``` - -6. **Handle Incoming Messages** - - Process user messages, extract URLs, and crawl them concurrently. Update the chat history and system message. - - ```python - @cl.on_message - async def on_message(message: cl.Message): - user_session = cl.user_session.get("session") - - # Extract URLs from the user's message - urls = extract_urls(message.content) - - futures = [] - with ThreadPoolExecutor() as executor: - for url in urls: - futures.append(executor.submit(crawl_url, url)) - - results = [future.result() for future in futures] - - for url, result in zip(urls, results): - ref_number = f"REF_{len(user_session['context']) + 1}" - user_session["context"][ref_number] = { - "url": url, - "content": result - } - - user_session["history"].append({ - "role": "user", - "content": message.content - }) - - # Create a system message that includes the context - context_messages = [ - f'\n{data["content"]}\n' - for ref, data in user_session["context"].items() - ] - if context_messages: - system_message = { - "role": "system", - "content": ( - "You are a helpful bot. Use the following context for answering questions. " - "Refer to the sources using the REF number in square brackets, e.g., [1], only if the source is given in the appendices below.\n\n" - "If the question requires any information from the provided appendices or context, refer to the sources. " - "If not, there is no need to add a references section. " - "At the end of your response, provide a reference section listing the URLs and their REF numbers only if sources from the appendices were used.\n\n" - "\n\n".join(context_messages) - ) - } - else: - system_message = { - "role": "system", - "content": "You are a helpful assistant." - } - - msg = cl.Message(content="") - await msg.send() - - # Get response from the LLM - stream = await client.chat.completions.create( - messages=[ - system_message, - *user_session["history"] - ], - stream=True, - **settings - ) - - assistant_response = "" - async for part in stream: - if token := part.choices[0].delta.content: - assistant_response += token - await msg.stream_token(token) - - # Add assistant message to the history - user_session["history"].append({ - "role": "assistant", - "content": assistant_response - }) - await msg.update() - - # Append the reference section to the assistant's response - reference_section = "\n\nReferences:\n" - for ref, data in user_session["context"].items(): - reference_section += f"[{ref.split('_')[1]}]: {data['url']}\n" - - msg.content += reference_section - await msg.update() - ``` - -7. **Handle Audio Input** - - Capture and transcribe audio input. Store the audio buffer and transcribe it when the audio ends. - - ```python - @cl.on_audio_chunk - async def on_audio_chunk(chunk: cl.AudioChunk): - if chunk.isStart: - buffer = BytesIO() - buffer.name = f"input_audio.{chunk.mimeType.split('/')[1]}" - cl.user_session.set("audio_buffer", buffer) - cl.user_session.set("audio_mime_type", chunk.mimeType) - - cl.user_session.get("audio_buffer").write(chunk.data) - - @cl.step(type="tool") - async def speech_to_text(audio_file): - cli = Groq() - response = await client.audio.transcriptions.create( - model="whisper-large-v3", file=audio_file - ) - return response.text - - @cl.on_audio_end - async def on_audio_end(elements: list[ElementBased]): - audio_buffer: BytesIO = cl.user_session.get("audio_buffer") - audio_buffer.seek(0) - audio_file = audio_buffer.read() - audio_mime_type: str = cl.user_session.get("audio_mime_type") - - start_time = time.time() - transcription = await speech_to_text((audio_buffer.name, audio_file, audio_mime_type)) - end_time = time.time() - print(f"Transcription took {end_time - start_time} seconds") - - user_msg = cl.Message( - author="You", - type="user_message", - content=transcription - ) - await user_msg.send() - await on_message(user_msg) - ``` - -8. **Run the Chat Application** - - Start the Chainlit application. - - ```python - if __name__ == "__main__": - from chainlit.cli import run_chainlit - run_chainlit(__file__) - ``` - -### Explanation - -- **Libraries and Configuration**: Import necessary libraries and configure the OpenAI client. -- **Utility Functions**: Define functions to extract URLs and crawl them. -- **Chat Start Event**: Initialize chat session and welcome message. -- **Message Handling**: Extract URLs, crawl them concurrently, and update chat history and context. -- **Audio Handling**: Capture, buffer, and transcribe audio input, then process the transcription as text. -- **Running the Application**: Start the Chainlit server to interact with the assistant. - -This example showcases how to create an interactive research assistant that can fetch, process, and summarize web content, along with handling audio inputs for a seamless user experience. diff --git a/docs/md_v0/examples/summarization.md b/docs/md_v0/examples/summarization.md deleted file mode 100644 index b817f691..00000000 --- a/docs/md_v0/examples/summarization.md +++ /dev/null @@ -1,108 +0,0 @@ -## Summarization Example - -This example demonstrates how to use `Crawl4AI` to extract a summary from a web page. The goal is to obtain the title, a detailed summary, a brief summary, and a list of keywords from the given page. - -### Step-by-Step Guide - -1. **Import Necessary Modules** - - First, import the necessary modules and classes. - - ```python - import os - import time - import json - from crawl4ai.web_crawler import WebCrawler - from crawl4ai.chunking_strategy import * - from crawl4ai.extraction_strategy import * - from crawl4ai.crawler_strategy import * - from pydantic import BaseModel, Field - ``` - -2. **Define the URL to be Crawled** - - Set the URL of the web page you want to summarize. - - ```python - url = r'https://marketplace.visualstudio.com/items?itemName=Unclecode.groqopilot' - ``` - -3. **Initialize the WebCrawler** - - Create an instance of the `WebCrawler` and call the `warmup` method. - - ```python - crawler = WebCrawler() - crawler.warmup() - ``` - -4. **Define the Data Model** - - Use Pydantic to define the structure of the extracted data. - - ```python - class PageSummary(BaseModel): - title: str = Field(..., description="Title of the page.") - summary: str = Field(..., description="Summary of the page.") - brief_summary: str = Field(..., description="Brief summary of the page.") - keywords: list = Field(..., description="Keywords assigned to the page.") - ``` - -5. **Run the Crawler** - - Set up and run the crawler with the `LLMExtractionStrategy`. Provide the necessary parameters, including the schema for the extracted data and the instruction for the LLM. - - ```python - result = crawler.run( - url=url, - word_count_threshold=1, - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - schema=PageSummary.model_json_schema(), - extraction_type="schema", - apply_chunking=False, - instruction=( - "From the crawled content, extract the following details: " - "1. Title of the page " - "2. Summary of the page, which is a detailed summary " - "3. Brief summary of the page, which is a paragraph text " - "4. Keywords assigned to the page, which is a list of keywords. " - 'The extracted JSON format should look like this: ' - '{ "title": "Page Title", "summary": "Detailed summary of the page.", ' - '"brief_summary": "Brief summary in a paragraph.", "keywords": ["keyword1", "keyword2", "keyword3"] }' - ) - ), - bypass_cache=True, - ) - ``` - -6. **Process the Extracted Data** - - Load the extracted content into a JSON object and print it. - - ```python - page_summary = json.loads(result.extracted_content) - print(page_summary) - ``` - -7. **Save the Extracted Data** - - Save the extracted data to a file for further use. - - ```python - with open(".data/page_summary.json", "w", encoding="utf-8") as f: - f.write(result.extracted_content) - ``` - -### Explanation - -- **Importing Modules**: Import the necessary modules, including `WebCrawler` and `LLMExtractionStrategy` from `Crawl4AI`. -- **URL Definition**: Set the URL of the web page you want to crawl and summarize. -- **WebCrawler Initialization**: Create an instance of `WebCrawler` and call the `warmup` method to prepare the crawler. -- **Data Model Definition**: Define the structure of the data you want to extract using Pydantic's `BaseModel`. -- **Crawler Execution**: Run the crawler with the `LLMExtractionStrategy`, providing the schema and detailed instructions for the extraction process. -- **Data Processing**: Load the extracted content into a JSON object and print it to verify the results. -- **Data Saving**: Save the extracted data to a file for further use. - -This example demonstrates how to harness the power of `Crawl4AI` to perform advanced web crawling and data extraction tasks with minimal code. diff --git a/docs/md_v0/extraction_strategies.json b/docs/md_v0/extraction_strategies.json deleted file mode 100644 index 570e1e32..00000000 --- a/docs/md_v0/extraction_strategies.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "NoExtractionStrategy": "### NoExtractionStrategy\n\n`NoExtractionStrategy` is a basic extraction strategy that returns the entire HTML content without any modification. It is useful for cases where no specific extraction is required. Only clean html, and amrkdown.\n\n#### Constructor Parameters:\nNone.\n\n#### Example usage:\n```python\nextractor = NoExtractionStrategy()\nextracted_content = extractor.extract(url, html)\n```", - - "LLMExtractionStrategy": "### LLMExtractionStrategy\n\n`LLMExtractionStrategy` uses a Language Model (LLM) to extract meaningful blocks or chunks from the given HTML content. This strategy leverages an external provider for language model completions.\n\n#### Constructor Parameters:\n- `provider` (str, optional): The provider to use for the language model completions. Default is `DEFAULT_PROVIDER` (e.g., openai/gpt-4).\n- `api_token` (str, optional): The API token for the provider. If not provided, it will try to load from the environment variable `OPENAI_API_KEY`.\n- `instruction` (str, optional): An instruction to guide the LLM on how to perform the extraction. This allows users to specify the type of data they are interested in or set the tone of the response. Default is `None`.\n\n#### Example usage:\n```python\nextractor = LLMExtractionStrategy(provider='openai', api_token='your_api_token', instruction='Extract only news about AI.')\nextracted_content = extractor.extract(url, html)\n```\n\nBy providing clear instructions, users can tailor the extraction process to their specific needs, enhancing the relevance and utility of the extracted content.", - - "CosineStrategy": "### CosineStrategy\n\n`CosineStrategy` uses hierarchical clustering based on cosine similarity to extract clusters of text from the given HTML content. This strategy is suitable for identifying related content sections.\n\n#### Constructor Parameters:\n- `semantic_filter` (str, optional): A string containing keywords for filtering relevant documents before clustering. If provided, documents are filtered based on their cosine similarity to the keyword filter embedding. Default is `None`.\n- `word_count_threshold` (int, optional): Minimum number of words per cluster. Default is `20`.\n- `max_dist` (float, optional): The maximum cophenetic distance on the dendrogram to form clusters. Default is `0.2`.\n- `linkage_method` (str, optional): The linkage method for hierarchical clustering. Default is `'ward'`.\n- `top_k` (int, optional): Number of top categories to extract. Default is `3`.\n- `model_name` (str, optional): The model name for embedding generation. Default is `'BAAI/bge-small-en-v1.5'`.\n\n#### Example usage:\n```python\nextractor = CosineStrategy(semantic_filter='artificial intelligence', word_count_threshold=10, max_dist=0.2, linkage_method='ward', top_k=3, model_name='BAAI/bge-small-en-v1.5')\nextracted_content = extractor.extract(url, html)\n```\n\n#### Cosine Similarity Filtering\n\nWhen a `semantic_filter` is provided, the `CosineStrategy` applies an embedding-based filtering process to select relevant documents before performing hierarchical clustering.", - - "TopicExtractionStrategy": "### TopicExtractionStrategy\n\n`TopicExtractionStrategy` uses the TextTiling algorithm to segment the HTML content into topics and extracts keywords for each segment. This strategy is useful for identifying and summarizing thematic content.\n\n#### Constructor Parameters:\n- `num_keywords` (int, optional): Number of keywords to represent each topic segment. Default is `3`.\n\n#### Example usage:\n```python\nextractor = TopicExtractionStrategy(num_keywords=3)\nextracted_content = extractor.extract(url, html)\n```" - } - \ No newline at end of file diff --git a/docs/md_v0/full_details/advanced_features.md b/docs/md_v0/full_details/advanced_features.md deleted file mode 100644 index 944f6e0d..00000000 --- a/docs/md_v0/full_details/advanced_features.md +++ /dev/null @@ -1,138 +0,0 @@ -# Advanced Features - -Crawl4AI offers a range of advanced features that allow you to fine-tune your web crawling and data extraction process. This section will cover some of these advanced features, including taking screenshots, extracting media and links, customizing the user agent, using custom hooks, and leveraging CSS selectors. - -## Taking Screenshots 📸 - -One of the cool features of Crawl4AI is the ability to take screenshots of the web pages you're crawling. This can be particularly useful for visual verification or for capturing the state of dynamic content. - -Here's how you can take a screenshot: - -```python -from crawl4ai import WebCrawler -import base64 - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Run the crawler with the screenshot parameter -result = crawler.run(url="https://www.nbcnews.com/business", screenshot=True) - -# Save the screenshot to a file -with open("screenshot.png", "wb") as f: - f.write(base64.b64decode(result.screenshot)) - -print("Screenshot saved to 'screenshot.png'!") -``` - -In this example, we create a `WebCrawler` instance, warm it up, and then run it with the `screenshot` parameter set to `True`. The screenshot is saved as a base64 encoded string in the result, which we then decode and save as a PNG file. - -## Extracting Media and Links 🎨🔗 - -Crawl4AI can extract all media tags (images, audio, and video) and links (both internal and external) from a web page. This feature is useful for collecting multimedia content or analyzing link structures. - -Here's an example: - -```python -from crawl4ai import WebCrawler - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Run the crawler -result = crawler.run(url="https://www.nbcnews.com/business") - -print("Extracted media:", result.media) -print("Extracted links:", result.links) -``` - -In this example, the `result` object contains dictionaries for media and links, which you can access and use as needed. - -## Customizing the User Agent 🕵️‍♂️ - -Crawl4AI allows you to set a custom user agent for your HTTP requests. This can help you avoid detection by web servers or simulate different browsing environments. - -Here's how to set a custom user agent: - -```python -from crawl4ai import WebCrawler - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Run the crawler with a custom user agent -result = crawler.run(url="https://www.nbcnews.com/business", user_agent="Mozilla/5.0 (compatible; MyCrawler/1.0)") - -print("Crawl result:", result) -``` - -In this example, we specify a custom user agent string when running the crawler. - -## Using Custom Hooks 🪝 - -Hooks are a powerful feature in Crawl4AI that allow you to customize the crawling process at various stages. You can define hooks for actions such as driver initialization, before and after URL fetching, and before returning the HTML. - -Here's an example of using hooks: - -```python -from crawl4ai import WebCrawler -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -# Define the hooks -def on_driver_created(driver): - driver.maximize_window() - driver.get('https://example.com/login') - WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.NAME, 'username'))).send_keys('testuser') - driver.find_element(By.NAME, 'password').send_keys('password123') - driver.find_element(By.NAME, 'login').click() - return driver - -def before_get_url(driver): - driver.execute_cdp_cmd('Network.setExtraHTTPHeaders', {'headers': {'X-Test-Header': 'test'}}) - return driver - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Set the hooks -crawler.set_hook('on_driver_created', on_driver_created) -crawler.set_hook('before_get_url', before_get_url) - -# Run the crawler -result = crawler.run(url="https://example.com") - -print("Crawl result:", result) -``` - -In this example, we define hooks to handle driver initialization and custom headers before fetching the URL. - -## Using CSS Selectors 🎯 - -CSS selectors allow you to target specific elements on a web page for extraction. This can be useful for scraping structured content, such as articles or product details. - -Here's an example of using a CSS selector: - -```python -from crawl4ai import WebCrawler - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Run the crawler with a CSS selector to extract only H2 tags -result = crawler.run(url="https://www.nbcnews.com/business", css_selector="h2") - -print("Extracted H2 tags:", result.extracted_content) -``` - -In this example, we use the `css_selector` parameter to extract only the H2 tags from the web page. - ---- - -With these advanced features, you can leverage Crawl4AI to perform sophisticated web crawling and data extraction tasks. Whether you need to take screenshots, extract specific elements, customize the crawling process, or set custom headers, Crawl4AI provides the flexibility and power to meet your needs. Happy crawling! 🕷️🚀 diff --git a/docs/md_v0/full_details/chunking_strategies.md b/docs/md_v0/full_details/chunking_strategies.md deleted file mode 100644 index f429310f..00000000 --- a/docs/md_v0/full_details/chunking_strategies.md +++ /dev/null @@ -1,133 +0,0 @@ -## Chunking Strategies 📚 - -Crawl4AI provides several powerful chunking strategies to divide text into manageable parts for further processing. Each strategy has unique characteristics and is suitable for different scenarios. Let's explore them one by one. - -### RegexChunking - -`RegexChunking` splits text using regular expressions. This is ideal for creating chunks based on specific patterns like paragraphs or sentences. - -#### When to Use -- Great for structured text with consistent delimiters. -- Suitable for documents where specific patterns (e.g., double newlines, periods) indicate logical chunks. - -#### Parameters -- `patterns` (list, optional): Regular expressions used to split the text. Default is to split by double newlines (`['\n\n']`). - -#### Example -```python -from crawl4ai.chunking_strategy import RegexChunking - -# Define patterns for splitting text -patterns = [r'\n\n', r'\. '] -chunker = RegexChunking(patterns=patterns) - -# Sample text -text = "This is a sample text. It will be split into chunks.\n\nThis is another paragraph." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -### NlpSentenceChunking - -`NlpSentenceChunking` uses NLP models to split text into sentences, ensuring accurate sentence boundaries. - -#### When to Use -- Ideal for texts where sentence boundaries are crucial. -- Useful for creating chunks that preserve grammatical structures. - -#### Parameters -- None. - -#### Example -```python -from crawl4ai.chunking_strategy import NlpSentenceChunking - -chunker = NlpSentenceChunking() - -# Sample text -text = "This is a sample text. It will be split into sentences. Here's another sentence." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -### TopicSegmentationChunking - -`TopicSegmentationChunking` employs the TextTiling algorithm to segment text into topic-based chunks. This method identifies thematic boundaries. - -#### When to Use -- Perfect for long documents with distinct topics. -- Useful when preserving topic continuity is more important than maintaining text order. - -#### Parameters -- `num_keywords` (int, optional): Number of keywords for each topic segment. Default is `3`. - -#### Example -```python -from crawl4ai.chunking_strategy import TopicSegmentationChunking - -chunker = TopicSegmentationChunking(num_keywords=3) - -# Sample text -text = "This document contains several topics. Topic one discusses AI. Topic two covers machine learning." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -### FixedLengthWordChunking - -`FixedLengthWordChunking` splits text into chunks based on a fixed number of words. This ensures each chunk has approximately the same length. - -#### When to Use -- Suitable for processing large texts where uniform chunk size is important. -- Useful when the number of words per chunk needs to be controlled. - -#### Parameters -- `chunk_size` (int, optional): Number of words per chunk. Default is `100`. - -#### Example -```python -from crawl4ai.chunking_strategy import FixedLengthWordChunking - -chunker = FixedLengthWordChunking(chunk_size=10) - -# Sample text -text = "This is a sample text. It will be split into chunks of fixed length." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -### SlidingWindowChunking - -`SlidingWindowChunking` uses a sliding window approach to create overlapping chunks. Each chunk has a fixed length, and the window slides by a specified step size. - -#### When to Use -- Ideal for creating overlapping chunks to preserve context. -- Useful for tasks where context from adjacent chunks is needed. - -#### Parameters -- `window_size` (int, optional): Number of words in each chunk. Default is `100`. -- `step` (int, optional): Number of words to slide the window. Default is `50`. - -#### Example -```python -from crawl4ai.chunking_strategy import SlidingWindowChunking - -chunker = SlidingWindowChunking(window_size=10, step=5) - -# Sample text -text = "This is a sample text. It will be split using a sliding window approach to preserve context." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -With these chunking strategies, you can choose the best method to divide your text based on your specific needs. Whether you need precise sentence boundaries, topic-based segmentation, or uniform chunk sizes, Crawl4AI has you covered. Happy chunking! 📝✨ diff --git a/docs/md_v0/full_details/crawl_request_parameters.md b/docs/md_v0/full_details/crawl_request_parameters.md deleted file mode 100644 index c32743c6..00000000 --- a/docs/md_v0/full_details/crawl_request_parameters.md +++ /dev/null @@ -1,130 +0,0 @@ -# Crawl Request Parameters - -The `run` function in Crawl4AI is designed to be highly configurable, allowing you to customize the crawling and extraction process to suit your needs. Below are the parameters you can use with the `run` function, along with their descriptions, possible values, and examples. - -## Parameters - -### url (str) -**Description:** The URL of the webpage to crawl. -**Required:** Yes -**Example:** -```python -url = "https://www.nbcnews.com/business" -``` - -### word_count_threshold (int) -**Description:** The minimum number of words a block must contain to be considered meaningful. The default value is `5`. -**Required:** No -**Default Value:** `5` -**Example:** -```python -word_count_threshold = 10 -``` - -### extraction_strategy (ExtractionStrategy) -**Description:** The strategy to use for extracting content from the HTML. It must be an instance of `ExtractionStrategy`. If not provided, the default is `NoExtractionStrategy`. -**Required:** No -**Default Value:** `NoExtractionStrategy()` -**Example:** -```python -extraction_strategy = CosineStrategy(semantic_filter="finance") -``` - -### chunking_strategy (ChunkingStrategy) -**Description:** The strategy to use for chunking the text before processing. It must be an instance of `ChunkingStrategy`. The default value is `RegexChunking()`. -**Required:** No -**Default Value:** `RegexChunking()` -**Example:** -```python -chunking_strategy = NlpSentenceChunking() -``` - -### bypass_cache (bool) -**Description:** Whether to force a fresh crawl even if the URL has been previously crawled. The default value is `False`. -**Required:** No -**Default Value:** `False` -**Example:** -```python -bypass_cache = True -``` - -### css_selector (str) -**Description:** The CSS selector to target specific parts of the HTML for extraction. If not provided, the entire HTML will be processed. -**Required:** No -**Default Value:** `None` -**Example:** -```python -css_selector = "div.article-content" -``` - -### screenshot (bool) -**Description:** Whether to take screenshots of the page. The default value is `False`. -**Required:** No -**Default Value:** `False` -**Example:** -```python -screenshot = True -``` - -### user_agent (str) -**Description:** The user agent to use for the HTTP requests. If not provided, a default user agent will be used. -**Required:** No -**Default Value:** `None` -**Example:** -```python -user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3" -``` - -### verbose (bool) -**Description:** Whether to enable verbose logging. The default value is `True`. -**Required:** No -**Default Value:** `True` -**Example:** -```python -verbose = True -``` - -### **kwargs -Additional keyword arguments that can be passed to customize the crawling process further. Some notable options include: - -- **only_text (bool):** Whether to extract only text content, excluding HTML tags. Default is `False`. - -**Example:** -```python -result = crawler.run( - url="https://www.nbcnews.com/business", - css_selector="p", - only_text=True -) -``` - -## Example Usage - -Here's an example of how to use the `run` function with various parameters: - -```python -from crawl4ai import WebCrawler -from crawl4ai.extraction_strategy import CosineStrategy -from crawl4ai.chunking_strategy import NlpSentenceChunking - -# Create the WebCrawler instance -crawler = WebCrawler() - -# Run the crawler with custom parameters -result = crawler.run( - url="https://www.nbcnews.com/business", - word_count_threshold=10, - extraction_strategy=CosineStrategy(semantic_filter="finance"), - chunking_strategy=NlpSentenceChunking(), - bypass_cache=True, - css_selector="div.article-content", - screenshot=True, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", - verbose=True, - only_text=True -) - -print(result) -``` - -This example demonstrates how to configure various parameters to customize the crawling and extraction process using Crawl4AI. diff --git a/docs/md_v0/full_details/crawl_result_class.md b/docs/md_v0/full_details/crawl_result_class.md deleted file mode 100644 index f66e16dc..00000000 --- a/docs/md_v0/full_details/crawl_result_class.md +++ /dev/null @@ -1,120 +0,0 @@ -# Crawl Result - -The `CrawlResult` class is the heart of Crawl4AI's output, encapsulating all the data extracted from a crawling session. This class contains various fields that store the results of the web crawling and extraction process. Let's break down each field and see what it holds. 🎉 - -## Class Definition - -```python -class CrawlResult(BaseModel): - url: str - html: str - success: bool - cleaned_html: Optional[str] = None - media: Dict[str, List[Dict]] = {} - links: Dict[str, List[Dict]] = {} - screenshot: Optional[str] = None - markdown: Optional[str] = None - extracted_content: Optional[str] = None - metadata: Optional[dict] = None - error_message: Optional[str] = None -``` - -## Fields Explanation - -### `url: str` -The URL that was crawled. This field simply stores the URL of the web page that was processed. - -### `html: str` -The raw HTML content of the web page. This is the unprocessed HTML source as retrieved by the crawler. - -### `success: bool` -A flag indicating whether the crawling and extraction were successful. If any error occurs during the process, this will be `False`. - -### `cleaned_html: Optional[str]` -The cleaned HTML content of the web page. This field holds the HTML after removing unwanted tags like ` diff --git a/docs/md_v0/introduction.md b/docs/md_v0/introduction.md deleted file mode 100644 index 6d1ad56b..00000000 --- a/docs/md_v0/introduction.md +++ /dev/null @@ -1,29 +0,0 @@ -# Introduction - -Welcome to the documentation for Crawl4AI v0.2.5! 🕷️🤖 - -Crawl4AI is designed to simplify the process of crawling web pages and extracting useful information for large language models (LLMs) and AI applications. Whether you're using it as a REST API, a Python library, or through a Google Colab notebook, Crawl4AI provides powerful features to make web data extraction easier and more efficient. - -## Key Features ✨ - -- **🆓 Completely Free and Open-Source**: Crawl4AI is free to use and open-source, making it accessible for everyone. -- **🤖 LLM-Friendly Output Formats**: Supports JSON, cleaned HTML, and markdown formats. -- **🌍 Concurrent Crawling**: Crawl multiple URLs simultaneously to save time. -- **🎨 Media Extraction**: Extract all media tags including images, audio, and video. -- **🔗 Link Extraction**: Extract all external and internal links from web pages. -- **📚 Metadata Extraction**: Extract metadata from web pages for additional context. -- **🔄 Custom Hooks**: Define custom hooks for authentication, headers, and page modifications before crawling. -- **🕵️ User Agent Support**: Customize the user agent for HTTP requests. -- **🖼️ Screenshot Capability**: Take screenshots of web pages during crawling. -- **📜 JavaScript Execution**: Execute custom JavaScripts before crawling. -- **📚 Advanced Chunking and Extraction Strategies**: Utilize topic-based, regex, sentence chunking, cosine clustering, and LLM extraction strategies. -- **🎯 CSS Selector Support**: Extract specific content using CSS selectors. -- **📝 Instruction/Keyword Refinement**: Pass instructions or keywords to refine the extraction process. - -Check the [Changelog](https://github.com/unclecode/crawl4ai/blob/main/CHANGELOG.md) for more details. - -## Power and Simplicity of Crawl4AI 🚀 - -Crawl4AI provides an easy way to crawl and extract data from web pages without installing any library. You can use the REST API on our server or run the local server on your machine. For more advanced control, use the Python library to customize your crawling and extraction strategies. - -Explore the documentation to learn more about the features, installation process, usage examples, and how to contribute to Crawl4AI. Let's make the web more accessible and useful for AI applications! 💪🌐🤖 diff --git a/docs/md_v0/quickstart.md b/docs/md_v0/quickstart.md deleted file mode 100644 index a0c1a2c7..00000000 --- a/docs/md_v0/quickstart.md +++ /dev/null @@ -1,204 +0,0 @@ -# Quick Start Guide 🚀 - -Welcome to the Crawl4AI Quickstart Guide! In this tutorial, we'll walk you through the basic usage of Crawl4AI with a friendly and humorous tone. We'll cover everything from basic usage to advanced features like chunking and extraction strategies. Let's dive in! 🌟 - -## Getting Started 🛠️ - -First, let's create an instance of `WebCrawler` and call the `warmup()` function. This might take a few seconds the first time you run Crawl4AI, as it loads the required model files. - -```python -from crawl4ai import WebCrawler - -def create_crawler(): - crawler = WebCrawler(verbose=True) - crawler.warmup() - return crawler - -crawler = create_crawler() -``` - -### Basic Usage - -Simply provide a URL and let Crawl4AI do the magic! - -```python -result = crawler.run(url="https://www.nbcnews.com/business") -print(f"Basic crawl result: {result}") -``` - -### Taking Screenshots 📸 - -Let's take a screenshot of the page! - -```python -result = crawler.run(url="https://www.nbcnews.com/business", screenshot=True) -with open("screenshot.png", "wb") as f: - f.write(base64.b64decode(result.screenshot)) -print("Screenshot saved to 'screenshot.png'!") -``` - -### Understanding Parameters 🧠 - -By default, Crawl4AI caches the results of your crawls. This means that subsequent crawls of the same URL will be much faster! Let's see this in action. - -First crawl (caches the result): -```python -result = crawler.run(url="https://www.nbcnews.com/business") -print(f"First crawl result: {result}") -``` - -Force to crawl again: -```python -result = crawler.run(url="https://www.nbcnews.com/business", bypass_cache=True) -print(f"Second crawl result: {result}") -``` - -### Adding a Chunking Strategy 🧩 - -Let's add a chunking strategy: `RegexChunking`! This strategy splits the text based on a given regex pattern. - -```python -from crawl4ai.chunking_strategy import RegexChunking - -result = crawler.run( - url="https://www.nbcnews.com/business", - chunking_strategy=RegexChunking(patterns=["\n\n"]) -) -print(f"RegexChunking result: {result}") -``` - -You can also use `NlpSentenceChunking` which splits the text into sentences using NLP techniques. - -```python -from crawl4ai.chunking_strategy import NlpSentenceChunking - -result = crawler.run( - url="https://www.nbcnews.com/business", - chunking_strategy=NlpSentenceChunking() -) -print(f"NlpSentenceChunking result: {result}") -``` - -### Adding an Extraction Strategy 🧠 - -Let's get smarter with an extraction strategy: `CosineStrategy`! This strategy uses cosine similarity to extract semantically similar blocks of text. - -```python -from crawl4ai.extraction_strategy import CosineStrategy - -result = crawler.run( - url="https://www.nbcnews.com/business", - extraction_strategy=CosineStrategy( - word_count_threshold=10, - max_dist=0.2, - linkage_method="ward", - top_k=3 - ) -) -print(f"CosineStrategy result: {result}") -``` - -You can also pass other parameters like `semantic_filter` to extract specific content. - -```python -result = crawler.run( - url="https://www.nbcnews.com/business", - extraction_strategy=CosineStrategy( - semantic_filter="inflation rent prices" - ) -) -print(f"CosineStrategy result with semantic filter: {result}") -``` - -### Using LLMExtractionStrategy 🤖 - -Time to bring in the big guns: `LLMExtractionStrategy` without instructions! This strategy uses a large language model to extract relevant information from the web page. - -```python -from crawl4ai.extraction_strategy import LLMExtractionStrategy -import os - -result = crawler.run( - url="https://www.nbcnews.com/business", - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY') - ) -) -print(f"LLMExtractionStrategy (no instructions) result: {result}") -``` - -You can also provide specific instructions to guide the extraction. - -```python -result = crawler.run( - url="https://www.nbcnews.com/business", - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - instruction="I am interested in only financial news" - ) -) -print(f"LLMExtractionStrategy (with instructions) result: {result}") -``` - -### Targeted Extraction 🎯 - -Let's use a CSS selector to extract only H2 tags! - -```python -result = crawler.run( - url="https://www.nbcnews.com/business", - css_selector="h2" -) -print(f"CSS Selector (H2 tags) result: {result}") -``` - -### Interactive Extraction 🖱️ - -Passing JavaScript code to click the 'Load More' button! - -```python -js_code = """ -const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); -loadMoreButton && loadMoreButton.click(); -""" - -result = crawler.run( - url="https://www.nbcnews.com/business", - js=js_code -) -print(f"JavaScript Code (Load More button) result: {result}") -``` - -### Using Crawler Hooks 🔗 - -Let's see how we can customize the crawler using hooks! - -```python -import time - -from crawl4ai.web_crawler import WebCrawler -from crawl4ai.crawler_strategy import * - -def delay(driver): - print("Delaying for 5 seconds...") - time.sleep(5) - print("Resuming...") - -def create_crawler(): - crawler_strategy = LocalSeleniumCrawlerStrategy(verbose=True) - crawler_strategy.set_hook('after_get_url', delay) - crawler = WebCrawler(verbose=True, crawler_strategy=crawler_strategy) - crawler.warmup() - return crawler - -crawler = create_crawler() -result = crawler.run(url="https://www.nbcnews.com/business", bypass_cache=True) -``` - -check [Hooks](examples/hooks_auth.md) for more examples. - -## Congratulations! 🎉 - -You've made it through the Crawl4AI Quickstart Guide! Now go forth and crawl the web like a pro! 🕸️ diff --git a/docs/md_v1/api/core_classes_and_functions.md b/docs/md_v1/api/core_classes_and_functions.md deleted file mode 100644 index 198fd427..00000000 --- a/docs/md_v1/api/core_classes_and_functions.md +++ /dev/null @@ -1,141 +0,0 @@ -# Core Classes and Functions - -## Overview - -In this section, we will delve into the core classes and functions that make up the Crawl4AI library. This includes the `WebCrawler` class, various `CrawlerStrategy` classes, `ChunkingStrategy` classes, and `ExtractionStrategy` classes. Understanding these core components will help you leverage the full power of Crawl4AI for your web crawling and data extraction needs. - -## WebCrawler Class - -The `WebCrawler` class is the main class you'll interact with. It provides the interface for crawling web pages and extracting data. - -### Initialization - -```python -from crawl4ai import WebCrawler - -# Create an instance of WebCrawler -crawler = WebCrawler() -``` - -### Methods - -- **`warmup()`**: Prepares the crawler for use, such as loading necessary models. -- **`run(url: str, **kwargs)`**: Runs the crawler on the specified URL with optional parameters for customization. - -```python -crawler.warmup() -result = crawler.run(url="https://www.nbcnews.com/business") -print(result) -``` - -## CrawlerStrategy Classes - -The `CrawlerStrategy` classes define how the web crawling is executed. The base class is `CrawlerStrategy`, which is extended by specific implementations like `LocalSeleniumCrawlerStrategy`. - -### CrawlerStrategy Base Class - -An abstract base class that defines the interface for different crawler strategies. - -```python -from abc import ABC, abstractmethod - -class CrawlerStrategy(ABC): - @abstractmethod - def crawl(self, url: str, **kwargs) -> str: - pass - - @abstractmethod - def take_screenshot(self, save_path: str): - pass - - @abstractmethod - def update_user_agent(self, user_agent: str): - pass - - @abstractmethod - def set_hook(self, hook_type: str, hook: Callable): - pass -``` - -### LocalSeleniumCrawlerStrategy Class - -A concrete implementation of `CrawlerStrategy` that uses Selenium to crawl web pages. - -#### Initialization - -```python -from crawl4ai.crawler_strategy import LocalSeleniumCrawlerStrategy - -strategy = LocalSeleniumCrawlerStrategy(js_code=["console.log('Hello, world!');"]) -``` - -#### Methods - -- **`crawl(url: str, **kwargs)`**: Crawls the specified URL. -- **`take_screenshot(save_path: str)`**: Takes a screenshot of the current page. -- **`update_user_agent(user_agent: str)`**: Updates the user agent for the browser. -- **`set_hook(hook_type: str, hook: Callable)`**: Sets a hook for various events. - -```python -result = strategy.crawl("https://www.example.com") -strategy.take_screenshot("screenshot.png") -strategy.update_user_agent("Mozilla/5.0") -strategy.set_hook("before_get_url", lambda: print("About to get URL")) -``` - -## ChunkingStrategy Classes - -The `ChunkingStrategy` classes define how the text from a web page is divided into chunks. Here are a few examples: - -### RegexChunking Class - -Splits text using regular expressions. - -```python -from crawl4ai.chunking_strategy import RegexChunking - -chunker = RegexChunking(patterns=[r'\n\n']) -chunks = chunker.chunk("This is a sample text. It will be split into chunks.") -``` - -### NlpSentenceChunking Class - -Uses NLP to split text into sentences. - -```python -from crawl4ai.chunking_strategy import NlpSentenceChunking - -chunker = NlpSentenceChunking() -chunks = chunker.chunk("This is a sample text. It will be split into sentences.") -``` - -## ExtractionStrategy Classes - -The `ExtractionStrategy` classes define how meaningful content is extracted from the chunks. Here are a few examples: - -### CosineStrategy Class - -Clusters text chunks based on cosine similarity. - -```python -from crawl4ai.extraction_strategy import CosineStrategy - -extractor = CosineStrategy(semantic_filter="finance", word_count_threshold=10) -extracted_content = extractor.extract(url="https://www.example.com", html="...") -``` - -### LLMExtractionStrategy Class - -Uses a Language Model to extract meaningful blocks from HTML. - -```python -from crawl4ai.extraction_strategy import LLMExtractionStrategy - -extractor = LLMExtractionStrategy(provider='openai', api_token='your_api_token', instruction='Extract only news about AI.') -extracted_content = extractor.extract(url="https://www.example.com", html="...") -``` - -## Conclusion - -By understanding these core classes and functions, you can customize and extend Crawl4AI to suit your specific web crawling and data extraction needs. Happy crawling! 🕷️🤖 - diff --git a/docs/md_v1/api/detailed_api_documentation.md b/docs/md_v1/api/detailed_api_documentation.md deleted file mode 100644 index aaf0e34f..00000000 --- a/docs/md_v1/api/detailed_api_documentation.md +++ /dev/null @@ -1,338 +0,0 @@ -# Detailed API Documentation - -## Overview - -This section provides comprehensive documentation for the Crawl4AI API, covering all classes, methods, and their parameters. This guide will help you understand how to utilize the API to its full potential, enabling efficient web crawling and data extraction. - -## WebCrawler Class - -The `WebCrawler` class is the primary interface for crawling web pages and extracting data. - -### Initialization - -```python -from crawl4ai import WebCrawler - -crawler = WebCrawler() -``` - -### Methods - -#### `warmup()` - -Prepares the crawler for use, such as loading necessary models. - -```python -crawler.warmup() -``` - -#### `run(url: str, **kwargs) -> CrawlResult` - -Crawls the specified URL and returns the result. - -- **Parameters:** - - `url` (str): The URL to crawl. - - `**kwargs`: Additional parameters for customization. - -- **Returns:** - - `CrawlResult`: An object containing the crawl result. - -- **Example:** - -```python -result = crawler.run(url="https://www.nbcnews.com/business") -print(result) -``` - -### CrawlResult Class - -Represents the result of a crawl operation. - -- **Attributes:** - - `url` (str): The URL of the crawled page. - - `html` (str): The raw HTML of the page. - - `success` (bool): Whether the crawl was successful. - - `cleaned_html` (Optional[str]): The cleaned HTML. - - `media` (Dict[str, List[Dict]]): Media tags in the page (images, audio, video). - - `links` (Dict[str, List[Dict]]): Links in the page (external, internal). - - `screenshot` (Optional[str]): Base64 encoded screenshot. - - `markdown` (Optional[str]): Extracted content in Markdown format. - - `extracted_content` (Optional[str]): Extracted meaningful content. - - `metadata` (Optional[dict]): Metadata from the page. - - `error_message` (Optional[str]): Error message if any. - -## CrawlerStrategy Classes - -The `CrawlerStrategy` classes define how the web crawling is executed. - -### CrawlerStrategy Base Class - -An abstract base class for different crawler strategies. - -#### Methods - -- **`crawl(url: str, **kwargs) -> str`**: Crawls the specified URL. -- **`take_screenshot(save_path: str)`**: Takes a screenshot of the current page. -- **`update_user_agent(user_agent: str)`**: Updates the user agent for the browser. -- **`set_hook(hook_type: str, hook: Callable)`**: Sets a hook for various events. - -### LocalSeleniumCrawlerStrategy Class - -Uses Selenium to crawl web pages. - -#### Initialization - -```python -from crawl4ai.crawler_strategy import LocalSeleniumCrawlerStrategy - -strategy = LocalSeleniumCrawlerStrategy(js_code=["console.log('Hello, world!');"]) -``` - -#### Methods - -- **`crawl(url: str, **kwargs)`**: Crawls the specified URL. -- **`take_screenshot(save_path: str)`**: Takes a screenshot of the current page. -- **`update_user_agent(user_agent: str)`**: Updates the user agent for the browser. -- **`set_hook(hook_type: str, hook: Callable)`**: Sets a hook for various events. - -#### Example - -```python -result = strategy.crawl("https://www.example.com") -strategy.take_screenshot("screenshot.png") -strategy.update_user_agent("Mozilla/5.0") -strategy.set_hook("before_get_url", lambda: print("About to get URL")) -``` - -## ChunkingStrategy Classes - -The `ChunkingStrategy` classes define how the text from a web page is divided into chunks. - -### RegexChunking Class - -Splits text using regular expressions. - -#### Initialization - -```python -from crawl4ai.chunking_strategy import RegexChunking - -chunker = RegexChunking(patterns=[r'\n\n']) -``` - -#### Methods - -- **`chunk(text: str) -> List[str]`**: Splits the text into chunks. - -#### Example - -```python -chunks = chunker.chunk("This is a sample text. It will be split into chunks.") -``` - -### NlpSentenceChunking Class - -Uses NLP to split text into sentences. - -#### Initialization - -```python -from crawl4ai.chunking_strategy import NlpSentenceChunking - -chunker = NlpSentenceChunking() -``` - -#### Methods - -- **`chunk(text: str) -> List[str]`**: Splits the text into sentences. - -#### Example - -```python -chunks = chunker.chunk("This is a sample text. It will be split into sentences.") -``` - -### TopicSegmentationChunking Class - -Uses the TextTiling algorithm to segment text into topics. - -#### Initialization - -```python -from crawl4ai.chunking_strategy import TopicSegmentationChunking - -chunker = TopicSegmentationChunking(num_keywords=3) -``` - -#### Methods - -- **`chunk(text: str) -> List[str]`**: Splits the text into topic-based segments. - -#### Example - -```python -chunks = chunker.chunk("This is a sample text. It will be split into topic-based segments.") -``` - -### FixedLengthWordChunking Class - -Splits text into chunks of fixed length based on the number of words. - -#### Initialization - -```python -from crawl4ai.chunking_strategy import FixedLengthWordChunking - -chunker = FixedLengthWordChunking(chunk_size=100) -``` - -#### Methods - -- **`chunk(text: str) -> List[str]`**: Splits the text into fixed-length word chunks. - -#### Example - -```python -chunks = chunker.chunk("This is a sample text. It will be split into fixed-length word chunks.") -``` - -### SlidingWindowChunking Class - -Uses a sliding window approach to chunk text. - -#### Initialization - -```python -from crawl4ai.chunking_strategy import SlidingWindowChunking - -chunker = SlidingWindowChunking(window_size=100, step=50) -``` - -#### Methods - -- **`chunk(text: str) -> List[str]`**: Splits the text using a sliding window approach. - -#### Example - -```python -chunks = chunker.chunk("This is a sample text. It will be split using a sliding window approach.") -``` - -## ExtractionStrategy Classes - -The `ExtractionStrategy` classes define how meaningful content is extracted from the chunks. - -### NoExtractionStrategy Class - -Returns the entire HTML content without any modification. - -#### Initialization - -```python -from crawl4ai.extraction_strategy import NoExtractionStrategy - -extractor = NoExtractionStrategy() -``` - -#### Methods - -- **`extract(url: str, html: str) -> str`**: Returns the HTML content. - -#### Example - -```python -extracted_content = extractor.extract(url="https://www.example.com", html="...") -``` - -### LLMExtractionStrategy Class - -Uses a Language Model to extract meaningful blocks from HTML. - -#### Initialization - -```python -from crawl4ai.extraction_strategy import LLMExtractionStrategy - -extractor = LLMExtractionStrategy(provider='openai', api_token='your_api_token', instruction='Extract only news about AI.') -``` - -#### Methods - -- **`extract(url: str, html: str) -> str`**: Extracts meaningful content using the LLM. - -#### Example - -```python -extracted_content = extractor.extract(url="https://www.example.com", html="...") -``` - -### CosineStrategy Class - -Clusters text chunks based on cosine similarity. - -#### Initialization - -```python -from crawl4ai.extraction_strategy import CosineStrategy - -extractor = CosineStrategy(semantic_filter="finance", word_count_threshold=10) -``` - -#### Methods - -- **`extract(url: str, html: str) -> str`**: Extracts clusters of text based on cosine similarity. - -#### Example - -```python -extracted_content = extractor.extract(url="https://www.example.com", html="...") -``` - -### TopicExtractionStrategy Class - -Uses the TextTiling algorithm to segment HTML content into topics and extract keywords. - -#### Initialization - -```python -from crawl4ai.extraction_strategy import TopicExtractionStrategy - -extractor = TopicExtractionStrategy(num_keywords=3) -``` - -#### Methods - -- **`extract(url: str, html: str) -> str`**: Extracts topic-based segments and keywords. - -#### Example - -```python -extracted_content = extractor.extract(url="https://www.example.com", html="...") -``` - -## Parameters - -Here are the common parameters used across various classes and methods: - -- **`url`** (str): The URL to crawl. -- **`html`** (str): The HTML content of the page. -- **`user_agent`** (str): The user agent for the HTTP requests. -- **`patterns`** (list): A list of regular expression patterns for chunking. -- **`num_keywords`** (int): Number of keywords for topic extraction. -- **`chunk_size`** (int): Number of words in each chunk. -- **`window_size`** (int): Number of words in the sliding window. -- **`step`** (int): Step size for the sliding window. -- **`semantic_filter`** (str): Keywords for filtering relevant documents. -- **`word_count_threshold`** (int): Minimum number of words per cluster. -- **`max_dist`** (float): Maximum cophenetic distance for clustering. -- **`linkage_method`** (str): Linkage method for hierarchical clustering. -- **`top_k`** (int): Number of top categories to extract. -- **`provider`** ( - -str): Provider for language model completions. -- **`api_token`** (str): API token for the provider. -- **`instruction`** (str): Instruction to guide the LLM extraction. - -## Conclusion - -This detailed API documentation provides a thorough understanding of the classes, methods, and parameters in the Crawl4AI library. With this knowledge, you can effectively use the API to perform advanced web crawling and data extraction tasks. \ No newline at end of file diff --git a/docs/md_v1/assets/DankMono-Bold.woff2 b/docs/md_v1/assets/DankMono-Bold.woff2 deleted file mode 100644 index 3072fd8567c7f38769e8fa161b92417f2630f902..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33480 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 MK{-$E|6vLM0LSWEPXGV_ diff --git a/docs/md_v1/assets/DankMono-Italic.woff2 b/docs/md_v1/assets/DankMono-Italic.woff2 deleted file mode 100644 index 1d01ea6d73be14be9ac9014865475ee73c597fb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32468 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 j0=7(^{K1R0G5BwXVt^ZBLDyZBYynI diff --git a/docs/md_v1/assets/DankMono-Regular.woff2 b/docs/md_v1/assets/DankMono-Regular.woff2 deleted file mode 100644 index 99c1425ce4f3f324c29c0d4ac465bf73ffa363e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30528 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

31>i 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 diff --git a/docs/md_v1/assets/Monaco.woff b/docs/md_v1/assets/Monaco.woff deleted file mode 100644 index e468c424971227557730c31799863d7cdf9edf7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20096 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 Negpu1(g`jA006dq^ArF8 diff --git a/docs/md_v1/assets/dmvendor.css b/docs/md_v1/assets/dmvendor.css deleted file mode 100644 index 0f72703d..00000000 --- a/docs/md_v1/assets/dmvendor.css +++ /dev/null @@ -1,127 +0,0 @@ -/*! - * @preserve - * Dank Mono (v1.000) - * This font is subject to its EULA. https://dank.sh - * © 2018–2020 Phil Plückthun. All Rights Reserved. - */ - -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: normal; - unicode-range: U+0000-007F; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: normal; - unicode-range: U+0080-1AFF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AADTEAAwAAAAAfnAAADR0AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYHrfRocGyAcKgZgAIVSATYCJAOFEAQGBYF0ByAbmH0F3BjqYeMAw/KGq0RUj2pRlAvOVOD/SwI3hmhvqPXWMbaxcFVodQ89qEsVRm4InDI6Gs3VnVb38T+eMKtHpJAeluqjdxfY5AW/2Aji7+hAhpH18LRTMohDxurb7wiNfZL7A7/N3v+faEVwiCAtAlNswsSqkzkr7sxFpbE2F4WxLF0avWJR3uYiEj23E/7//948+9z3axuQActQPCviQYu1JhCmVuh5dt77WUKB5zYb0xj3A62tv5pdfs0/mds72PnOmAWKcrQJvdBLbhG19MIayF60FaCoVEvGmoFB5CpGov1mfcv9SVvt8a3BNzNJWdFTvZnl3HKm8v8lojXlwpz//zTV/86fzr1+pPxq3tFP5bU0YVQJFSCH8NdldEEuuzRFyE+cEobADpqDiltbjmznuokcaPoc3vfmjPrz3/8A1Q2fzsM1NRo9YABbvk9KwBmpuqGbkBNum2Pm9NyEBWCXwrUppZRt/3790tnL7UtoVTiUOSvJ/su1t+m06lrTTe4ZF4uQSDyOQWOs4P/fL9Pev8suydKsK9PxiyhshanNf/8n82dmCYILOFmeZAk+vh/gaZdeYJK/nWUiVSA4Hi3DG+daWWP3VLW2sk5UycoKSbLLnApkQEWbzdO7x+hcD47hVB5kI7ZqP8Ml3IABvF8ggEMBQRDmiWzS+LjJkya7fHqROvwx+vO7Bl5bGHjzwK8YsMMNMWmY4PG85fcwjFcbvqARS3+2OwjIJ4193M0Mz0mhw0U8mPnNbv+UMyb9AwjggApsDmRcePEVLIpRimwFJphunqVKrVdjq70OOabRBZ16XXbdbf967KnnXvrgk6/6pUEAwRACQkboiA222CNAipKReOCFFj8CCSeKWEaTzt8UMoFpLKSUCrZRx14Oc5KztNGLmds84g2fGESGXaPWHmGRVfa2waGO9F9Ocob/8XjP8lKXer23utZ7fNinfMYt7vIV3/K/fuxX/ujv6KBcvFhwCcm58KQXKExey8jKwZujdxiflArhllB2Q7/XPvpuKn5q05C2uMlS5JxnPhmyZSrVbNC8Xa8dew8nSJ4uS+5CJSt0qRv19bhXfexHuC+yKhvHmWCOc57nfBa0+HKrrrPxFnvZ+/AEk0870xzzL7byx+e7L7Sc3hpOOMREzBx8tKQoQHI/6V4QnnxeBGVnVAsOAYkIogJzCM9jDiFynjBwiM/l8y5RU6i/akTP4UWA+trG6wXECKBnipp9SRGgfKeK2Rpe7RisSP9gAznDFqJQsOM9P/FQtn6Xjni2dEET4Qp8wz87Wpx8iqOv5WZl6LftqlNrYOwlHCUmSssZa9yiDo5y2ylGYQUy0G6PxN/PC4NNgjWo+uRLOrIfQ+ulcU9RL9FS2U227AfPjpPsm9ONStCVzViZ3jnbqfv+jEpfmzg7ugVTasuE4QKfRtdf4PaNyS17Z/fLpYBqNVcJxVAgWQlOrdwa0zmjRqehNBAhroo9Le0OUW6DjEGXglh0fwZpRX2/JdIWZWrMs75j2uwglFHPw27UP7CcDNT9IQD3gwXPdF2lPsf4GhNFqbzAsOncxUuuGV9epFYYArSFV1zeAouss5toxjtvO7mvbtVixUPtJl6NLdyDnfseE6/EH9+y8l/Xf8dK/9+ktcmgzTSVnGXIjRLUPJuBi2z1Vbxl1yEktjC5bOE0eNhs5Quiz0aa2hdbfqbFCIAVEDdwoL3FsQqcjjCKKEShRM5jAQ4JBRPbdWQiiALEawKM2HnvEQgYROicVPNWpqy3CFYdD8DQNmC0cMpKtU75k1kFUAKZo8fIo3bL65k2bGJS0U32XfVYwx66fxTvKvyr9uWoPXhpjP5CRn2qazK/lv5vhbz/mycWnnTIPi5YmwhVqURjwdlFZss5ASDw90ARyFdggkJYDvthHGyEXbAEamEbLITpMA1WwhzYAVuhHubDXtgO1bAB9lx1J6REsE6ocqAUFrHTtDRYweZgMRTPoexImyDnS1g1oRwOCweK4CgcgiPCsQanOjw+cAY0WnD2qs3gXNBiugA6hbYGXXAZeuEqXIIrJjO4KVxLw13hVod7V30A+oJ+033w9PcOHg08C+rAWlAJ6epgC+TWsVbwEEpnQDeUH9ADXkBFhgqwBio9QA1oAPugKQUSYUAAhQDlIch0zvDIBd4espgUKxMDJ03pqTjtTH1pEB+Z3cwN+aVidvGktC1zynvVtOoLRVUvrD83s5o9TU+zW5BD/7zg38P+fde/j/zz8L9C/+r9n6v/I/1Pzb82/qs9JENXkUXksVB56JzVwP8m/1vVqazT9k6/d8pZ09exX5dN418wd5rLtMRpu6ZdCfcwdjfeZpxtfN/9l2tObaMKqbF///3f877x+KbnG872WkG+oD/ifZv67dFv7/1P9LvI745+d6/zBSahJlUmwxd6pjtM39pfLBQLi4Sv8q+nW/83839/TP96+obpW6a3MLyYFZPAlDOfhPPY+hn/7BI341XXm2YWznzcbfCsHbNOd794dtWcRXOu9yyc+05+/7zB3tfNb1ngsCB5Qd6Cob49FjYu4vcLXtSyeM3iksV/O09eUrd0xdIjy2YuUy57OviG5aUr6BXxKyaHjjI7bGbwmGvesJJambayd4S3KnJVtcVsCy+LZot3mntWl6wRrvFd02m5zFJ7yeDvb156sdW1yy60zrC+fnkvm59tamwmr+hjG2FbbfshYKJdvt3YVfIqS3u1/emrThsGOPzqMHCNaO2utR+Cx607uv7z9fL1565fsqFogz7stY1c+OObbo52c2xxWulU5LzeeXCc2OXq+C9dY914bhvdctweTvRzb/RY4bHNY6zAR9Qm4ox3eHZ4/r/g7aXx6vTmea/3TvZu8nH0+d2n1edD4oO+R33HZ+zyN/K38E/0z/PXpxQHXJu9WNwVSAbGBJ6dax20P/izlnNwZcjykHMLNoTuC5383x/CdDdYS2qk06WHZf+RyWUTN82UB8k7FEKFXHHqFqPwdeG54TdutYj4NeLJbXaRxyP/LuoeFR915vaB2wdvn7zDJrooxiJGHdNG9ubagAczDDvBZNJp1h406izKxGE8J5+2zJSZMIx3db0RQooCVUMDIYHgRr5CrlLJGRIHlV2PXYT6ht7gPs4gIlUJAYIE/qPmnYsn+35iYrNNFO1AI+PGCRAAX4MDzu6a2x3z2KdfJ49dWtXvI9O3mu58Fj66CdnsGSX9FMpSeNpkIUnZbbhGQMQL16XZeVsYXcYlpniYuG3/F2PvD+YhWuxNORwrGl8N3AThn/uxQEj/uRfzeNJbkYCeXNbaLNM+KC1jerqeVd/tUmtJIz1hg8Ibd8p2ytgdqh1pKuEMXmhfA/b+ZoHcjrLDNui6k6Z63xvuPBabJQsWBh2W5ccwq1adnXr/d6eouuk4DtoaJVonmhhZUTFSOUt+kodSdMf2ZlUVo2ubenbECKL0LtmhUDpEpkmPYcHqRsHIygCLVXKxlZWs+37bqp7PXgj0VCZloQ5vibmiJEGA4jKUGCIyXYEOme/qqjJq6SKBKh3jmPF4Ig9yIQZ5y0OcqdU+Zw7jaLtbCEeNiQy6dq2TQwxmcvMMavzXXS1D7Jq2fle4rmyE6rXknxEjtxCiLWgFQDvTusDW1j9sgZV9efsoI3Yn/Bs+5HfkQ5FVoFff2i2uZrWLq4M/orBlWjXTu/m3DTZtXjvZ3HhvSCPTfcnZdPOshnk0KoauRnpBnbg7hgXX2YI7bc+sWBr9A7JHm2U/PPLTQYbtkcYTTAsOYDaAaeOlmUbyfFVjAyHBwEa+QpGmUjCYGbHfchITtu/djPrNO+qsuuqaCpyvsvHQfCB2YirjOU9aYJvgBAWgYB7yeI6s8Ay3z2fC4OfQNNOhwyCYjgxizTfhAxcjwXuMZAr9LSE1kHZSNj3Y//WZnWsq1xT72TOfEZXFPs0b6Tkh5giM3zTmZeuWdTfeZ8k9erQ1qRNseA4b2jB8b6OqLT9mC95lbj3TeYzI3sr3+1MXpy/eNcUI1lTAHFkSm1KWjszYehMeUUBMQBPTMiKE+9vhuf7u+r7tVqoGaBlWVtsp/ZxYDgpTIYUMJPSCcsg5jGUs5dz/algeu/iGANCDB4EBOziMUcaGLUeMsWGPgcUAZQZ1DlaI0vtyozOFWvHpo01pVUzRYSVx0udnl4cwS3hH5guqTpzNK9W/RrHSuyJkFNUWYtFk68kmn7CpAJpESK+oQvwW62A9fFM2SvQKGt+bamlOXB8Zd3T5fk3vqHGv6vyZCdUXqr+tym75PlYkUsb4suTWNGIbLRxptrO4TkhiZTOUSGvkNCguRQUniULeRHZcUFqGjArjLEjCHluPMacotI/nsua7AII0/iCMTzeGHgVEuUuhhzNJV0K4SZ8UQP6SSFVCnJvE8ySOfX6tBWqi6LprCAIfZUhUgmlhJ5g2duImpa/zjKtqdoPFUBbmUuvcpRyJq+hJeU2r6pZa5wTlzz9fIurmwjvOZF4qspwK4dYeEpuERlB9Yi9BqcryEqsOlG+zMhkhq67du5yfTmoBcXwHPnyddlWmW7e0Th+9qnnMhVt9TG5d9p08P1QVqpRvsDZiw1SYXFijG417RMgHp7AZ2vGZc4rAc7ufYQzSrWjd5s7iig+Qe3jlGekGT+xY7cLP8/V8DXR3s+RBgAmOJw8FPMGR8vEy2FQbx+3HcO4+Img4Ybtxn1Ms1VKuAo9sxefpuH7qlLh08Q9GIX81s33VV3zmpCJQMgQJDCygUd/UjynYzX+I9YB3JwINWxOFbMMN9g6aEIeJky21Kxso1CPENZCJS41LSUo2ksKh1f08WfiFmTjN1LZ15zZ00V2iho2igARFKtO0maj+yiHxgvbiXSXr93to96qXRc31DHFVGNZUzrqA2G3uoxrQ6yga3/Hz+ZXx1eHZLLVdU6c0ONz0udEnncJCtyZvY8oSiLyPzUIPaKg7GYlQprizqeE8KlNT+HzHj6kSWbwmIDD9YPkKG5tCUCpNRPHJ7Uv5GM1fCRLhNml+Vzaldm+GuzdTmKJg+Q2HE2Jiestj3G0ww08USGB2f+hqy6h91LCyqryKod6/2L6sgyFEzXj95RHrfRk/ma9sjhB5RyaDA/B/ruj9hvHqIKZF26a40RTnOLdm1MNK3UyNqny5QqWSK/JVDQ16EoAkrPDDggt+2XiIL8Q2YM4NAhtmMotZ5UHAAgtsZpW19W1hCzbYczqJkA+jP3S5fsF79oWPKv+vVwcgMgLGrEQK0L2wAa3vEDt5qBLuAkiN890DUybp+uqwk2AZVPLPnfPWr3cKncGSzwwYWYMFYtuu9v/8aeOyOUrIvH4OenmGfW36iARtaW1d9TovUcSWlAAFE50cnRwg9GpWdaQw7Sk9ibZC2TZ/l5SY/TlrTKQLoappP2qhAVfddPLt3HqbRtavZFtOAV1epLbqYH1p3HUf/X3MJNNjZLIXCbuQXW/hdMbWRJQRVJQ/R8aMx8wokKqkm5mGzUFF7vQG/zjXOLbbnTj/SznJgOsfR6KLtyMra8dWegYSC3FE36vjX9atLel8zboVVM3ezs+u6k7bULjWmRmbEudBuyXXAN3ToYO2fumPSg91AuIvL1bFWMd/aHy6NFZBR8RWXNy9uvzvhwfwrRSRyREY9eNA8mu4ohVflZuuQA2h99mh86GuQPpmTHydAYqE+nJAh92N1ZeH9g2+0r5jQ3HKftMol8ZM7cM9saarN5QHAKk5M/HapW8tN6VaT88VV22g0bPnPHT3YAvi6Ma2213a2FV85vb3sV1sozvzo6n5uKAWsrbWQNOTkuBSBB7HjAKSZLA6v9D8hWt8g7FVlp7SbiM+wrnJ8i89/m7+8a5LerEGnJykTpApE4+WbTJSPyKkoXq7hR79ra7F05iEpACcWJFwiA1MYnYUSW0iKdXFYBah5MhYHY4oFKtpt8NbOzD9mPvzac0j3LKPZbCPUelFBTyxDsFqxXrx045Fz94Eq7xGdaV46fj8lrvP50GreLVwaw6tp+xJoFExFWW82YPxeEqHPluMS3n+A0ttrCNfYA7mL3rz3gslB3RqvfVDFqMaoW6y8VGXUXu6W50nPccJSRzHsTuZdObCBmPCi8JyTt1/J7++rLT0FRvcRSxITNsuptduKQGGVw0Ly/kYc9sQw7gwpa+aQUEQT37xOsgh//7+lsp185HDdgvmebGwU2dS57Ex1HioFpB8daEmFtIiZeMwFxJQM8dA9LgrnD2BD+QXnaBYxv2knmHBReceYuxjiKXNA7fcyQ8ZFowW+OKFDV/Dotsie+ZrV6desBhGx5FHGjFMFo1wym9CNwQcO1jPmzvT7IPJjsb4mqNFuO31SKy38dTg6Un97gPedTxRdLSXKLzubOw6nkV/4lmsvWauPBXP+57U5rXAX/s5xm0DbZuNR9pMO8vb2rvmtTk6Boc6OXaGXffWfnVHSnmd32D1ePXPnPy9UUayJI+vt3Buu62z2Jp+yMz4cTOtTjpQTEjIFkE+vNigJGm4UNwW21VkFqoskDSS+ldvv5RhDKiOcT2hwhwlUZByUzSrvYXo+tqqtIHSowfbclmqNiOvcLOD7lvfEzcgvzqiMC1eFUOrs/YWrLCU5/H01HwxTZraos8hCH027hkOnRUgQ5A4TnAFu9v4tUWyJF4lyqiovMcEzQIHlY2U6qzAj0kTOTT5BLXRxM0zFLUUTDxQFd9WejlrTP8EoT1KNKbsLWBhmgWSDhtS40dJe44cFNJ7dmMWD1X4f0Glu7RfKy4kZwJRQl1569DoYRP1Zt+ZO0KNfvOvWVeQ5DWYYMHZDhvlPJfq1HnZtZbPOj7s2ssSiujKzcfi4jZvjtu2O2vfT3BLWIL3Lz9Q7UYuVzDLSoDsno7ytAUayonTpwO9CKE0ZRlrlFJvYElI0tQXff5m4Apb15uF5F6Hw38MO+4lSEZMkwVEh9ncDBNNBmJuQ80tHMmUOyFtQcJqAMPeZfZSEoY5OrEUYQ5peFCyEmHsAmb4BZgYD6ZriYpJoyhveqLEkJ0lN5usb4Q7XeD4xhR0pvQi35jSgDRCSS9gigPrhau5zFMcneApxuQwVfA/OZODTYZH2TxX+nbIODxWkL5Cs8JsFc4qVDQ/NuKetjRFoDh2BLKOeb1Df2Aprv2rgth9FLxLL7IuhiE5k8BMzSLqvUVWzV280NN3FosqNAt1PmZnpctZRPm9lrZhUUj1SN8Y/eEwZMot5NYYE+Fv4eZNmZQxYrsM+Wkz2v9c442JVvN4Xm60Y8edS0vep/sIo4I+J6BLyVdnW2MBVTbQCBeh+Bt+QaPiRhTjmwoiCo9BxkAG0XeMkYaDOZdxNBmocuBYc/szgb8M+Vqd6E1wseEhhX7lzqayhyKBeB+GN+iXbja+aZcli7GB0JSlPjvOd8UGAvdL15IjxPmh5AiOZnC23trUgYEOW8frYMKpl8FUgfgAIQg9YemBVajvBioO4eCeqOB6XnN8qAfCvJ0jxLSCFxiLldvPWgSIdV24hIumLgqgFaUMO2XhhLJBiHDhQoyA3RiQCoIwdVf2QsQpx+YgSZY+pjcVff5Q2dmIwk7xwwrmGPc4YAn+qTuhTzs6jBfcHuAcjYvzQ77/8kIkKxBsiIjPEZ2scDW4xJicOC+kuXC8l1sQhsAdsD/gPgDSiB6Rbm1zn1zGxI1MyGsTyzI14K4Dbl4OUcY0kD8H+cMx7WZ7Wf8QpM0Cft6IKZMI+Qbx4RmeUOhtd5dORJRBBHpziNMu3CS9KW54I7lXv3GG8EJ+BtvSOJCx6jIULbXkiMdpevh9JX6LXRiINRTU9Yx8STc8SoatfTH+vybBKMIX1W5DF9Gnoz+XHs++1F1QnDPJEK6k/5NpHVUwN9tHjYmmTApXshKxoNdCD6d+T1nhoSpgETRDJU93T1+42DMIiR7pK/1LWQlyGx8JrMNWR0lehy0OHqkgIs5DA7dzA/c6EWhPT1k4ikKj0wFBfi5dQScYnwo2iMh9NcEBZeMA+oj7FPokdujMB6YecIdaLjGh7N8SXjsMjYI245a1/pMQRhxkFC5JZqnuqITL/z0EDiFRMIx8OzT0M4YKeRAfxmf35Oq5GBrlFkVQMGMDIk8zXJgEwjOdLmgfNajfFIBt+TXgPo4ZQiXCZzP6wI0KZ7+ht9i7nQ4IxgR81jexc44K6NBek9sfO4/Rdmg34TtDeOzHPXXQUyLtwykZLfI/x0lllxsTYYpE7cHeUD7FYLdCt4Ty0wcd0Ech+TL3tWHOcUd4huEEcYNAdskWX4Y1zFeS2jZltevblNv2+OfP61XyjrrOLW0+xMCOgJWd9uK6Evb057UPrVEwruhjNgv7W6B9TBfEERcnn5d1/Pm9TGq70t1nISPZ2MID7PPrau4TuGRjQEn7IktypsH5kIehsc0thcbyN7iuDeukK31rmforRIshIc+M9xTK5y+xQDmHak5nsx+IkK8fxVX4xBOcaneXwjfdSlc6ydzUbONPhs1CZg2v9M59aXFCw0yl2dJNGvyq1V/PP7jzim6sYUu7XinhPxxobBsjk33Ga7uVOYpLR+2Mtidc1MKB8IVfKwxYgMVxMLAP+1yMAbwk9vw7ASS+3crQTvFOn2Ymx7/N/mktQP8SGWLd6JlSIMCnhCV9bfb/egQvbaZ27zpyh2T8uglvZsXy+p1Jh9LaeqMak7BLDcz6mv/XSyCoy5X7LZd3bJ+7N/krb/l1FifET4fNvJBwhMBHfunATgRlEZ0uFV99bBfJICLiDRFidx74R8BDW+/gkXvZdfla05di5uPumMEstN389IYen9wYG5OTWWVxFS7ZsUeijkZgdjC3M5nsu5p7GE4rt3BAm2MY3jk2y+RdNS1bXfSF6m/b3cgl99azjeDYFWfcn+UMwePocUOwpPICYhMS3Wlnl8p7D7GUlON7x+oXj+EpB26CNlOM1UHH0Suqs3U4VwNjgs7qMrzDW8vUBiTE7Ac8mbyjs1PWrmN4cszGw6QAPE9bBGIQ1R2sWMNevZHO0Rx3csiBp7frDnV3HYEjJDXZ0z9qhrKMZROt57KkLk1ZELXY27SZEuNBaYsuRR5fDXXvSw/9/h8NejLyg5QTKh7RxgYro/Ba2dBzJkGJvK3pTNHlfHictlyABEQOej80ghPGmmGKZ1gvlV5PICrrBePE1YuZzfxrFs1eHVhy8UGGTA7ruIy1qwPdHfYuNBHGnRr6FxRQnLF0Qkz2wbs3dDNvCO9lHD4r2Lk34jnagwqMpxdbr0ExO062WrZUNA4pRBW/HfNCHz7GVaQs7Ff2mYFXCAx8VfikFqLG+0qQKMjkPV1SlJdh/LIdZzLuWk6q22gr+36DxCuFy+XRZojNPnUghv+G8EvJukNxOMOxIQ11jBpofxAoM8ExRj49rMPVUxnGnTrIFZyV21wG5wn/2dcdzjGMf/SoP1+TJHg2HUYqcuZ/HPpbTx8bVHnYKAfsFg9AEaU6Hy56/tNEbCjvXvvN8xYJ4TrPJWiU7sjBz05v2/mJHarY7/5wtI6RnGlqgp7HCtK5+0XG3VTPQa8W90oqDA4XC6hd+JeSrh0YJfgtugtrbI5qXzlfci0W5703areHnxAT5yxE9FGGWxE3FMvuR2T3kRWFlDimSioMt+1kDPqnGVslmJdqeui+UtfZlREllltjgXGiiw4x+KoeYhQccSBU7CkmQ7GD1SrAxBOGRXQZg87HopkHnZCfsnRTxBpo66dLOcOJHY3BHJ7gW3wuUKDvO2A6thcQpWfAHTMAyiBc0JdJju4jFy8TZLGxYaRSdS8DbGVum8NCiv5DvqDajd94D1JHYWpCNkqsdmTFG7G4tc471E6st2fFyoVmX5L2/SZEYElEw1ZGw0GGJv2KPELkIaL5IpcRPv43HbVRXfImDelOhfCgZHyvT81zzjlvvYV5AFhg7j+2iF2UTRRorPysaSSNBRwykbsXXGC7ds5xOc4+/Zo4dD9ijV3yUcNEDuY1nNnAUoy4jbXkexrFfIIyFvbSaJ39OCtNft82ku8doq7ou4vIA9enIrTGN2eSsDP/dTvtrLoDquHiynPWH5zIRJNSZNg3T3DhcqRn4HfTN8IicU73CxoOmndDEMt+pDPmJJiiffeacB0VenfymZHf4/IN0rrhAA1TSpyF/t4Prtc9JU27AO9cGJolfWm+SdOpGWUSY4nBfnymFiWUhuOhF+RHkQ7mCbafDECa9qNDCR+pr9Fbxhc3uTXJ8T9vdurfKuxmFBDatw2FDrr9WdjHuGvx4JLs5Ri8dMmGjtcCfSkugU0x0kaa4XFSsHtRvJDgmiQwVAyRc9QJBzMjzryzcw9+ftLi5KdIbZl8Y/K1eZqu0EI2r3xxFt+29BxlZ6Cgma+mgU1mz09dVpi8Pf7H7cxHS6OAqivg2fki22gPH6bbnbi0fcuibvD6xhUMXgn/9cfSdyTTOJWJk5Hg3Y+kMt9A0OJ8fEvgcv74UJt4R9rRp+K896VefGA8g7IT7pK0TdFdLCBZ4hHH8gTyUAgNkSa8yVUNYWqkbgpv/qTHwhDH0ujw8JJOsYw84ci4A1t1lQmlkavFksa+HRI/xmqOE3f05aVEnhtXpmZcszzjJMHd7ibqzGDGrSBLC1leXSGyeEjHZaybebFE0vbNeQcMkDeA8xW7MIe4OtM33IpGmHYTXCjKHwFSPbc6nzCyrnLk3vmXmfCl4n8RCjiEXXf2BfUBlkRZIxIarEybnUEZoep1AEahzB9v+vniNm/2JWeiq59aTM/fOHBmlcepxOufYoWCBvebQciJ7D9kBsbG7n0lAPLbax6Az3/TMgnR170/1QQ26hmDlFZqA2zC2/lV/+shYQ0mEJ161wVxA/SAckBSTFd4A3hchOYpC3irTNEdzCCHzeRjo3mn6nbzntZXY6Drq8sV2nsCNKWIwxMtpEUai9amnkof48r2jNLtjubxwFqy20+gTfNqYS/qKvIHe/kdYfFQvHnMeG4D8yVVGQcv6HS9bXRKenEuEljMLdkqKQCH+n8zeJ5hqX+zLmBnVhuVGF+GhXq00CNHyCC2i+OPAf15ThcrfbEtqT8PaVKp97cjW/Xdks6ZAS24t+pG5S6E1qTg2p5fzvzAUvF2QYO3Bm5N3OAs3VxnhTLKg17qcv8PD/1DuEfyyAPMhk0MwTwzL2Xbbn4P22gPjkTxMa7yjOWaMWtutTkIYWw58iKOi5hQjMcJI0zS0ZcQYZi25+LWw3tI5y7UCBCt5j65Y5/dG+ADqBNeCqgjmqcPOh0DjoWm795kKTUJmiQjnzRlbDD980+YfYYynYnfP1PcSsHwxubfS9mcGqVP4aNeFxlkH8CCrz6QBvCeXOWK0WMxOAEjr4O4Jz+SLuKVMqeDqxS75PXRG7RJLzV3QGgorvLLZUz+o7SyoeFcgrXy9qazj13JhxscPMchl/sneUpZOBj/ewznwo8prJV7ll5eeSv/aTwsxaVnH2DdzE3hfjvn9BgmLeTiLbk1zx6qbz/U5ntlGyW/B7Dpt7wvuj7WbYkmim8JyfGnfbQFesDhPErv+qFzIOqebeCKnw4TSxnMCe4TVusNTum0Q7bzhO7aWG+KPlywzIzaXdiFI4TY0vjV2YwNetfcMgK989y3zgs3GMsbh9w6Ma6HHYZuYp016p2P7ondG0kP7fl6IGY9JdZtPVMfHdbLq19yeaqBZ+cKyhRHPNcLB/hkmZq2+8A2A9Uxq38k1dxUwDf/a6IXObr6QJtNnPUzKPUEJaQUGvMQL4JYo81ciIHGUafLEEEoYWeLTgwNRhDG6Ioc83S5483qUaRnIoLs4qcfe7gHNOAe2wN0iYc4Y92omdlMNmkK0Y9PBt8cvgcJeoWVxHOOajik/MRbzLofngcBmkNBf70Qruj2aZiWA5nix6aShiseJAWgMm5qBl7EJzR1SX/dt3qorxCULUMChf3n8+2gpRSetNoKjn4bQaWEtgUcbMee4YDwbG/+VAxx39BPwrpyMOdy1rQZM1mxfge7nfsKizNoanRgeBNpbl4YlzMJpWUETc8BVpVD6Q/H6+zv1gJUP+NrtZzGACxu6g1UPLIgnats+K26AGxKMoyvK7xW7prraCNOOD10Ady2V5SeDEEqQ5pH8e5fUpWr9xJ8XFYV+pfY0Og2aBZGelCOQrpVd9OmWhbXyqlJ4kRLevbaF+CDvbP1hNBtQT4KSARN1RJvD7kL1R60+p5ezXg98k3V3CTiB+RN52omQjGXbsOrpIC3RIF8mVLyA1fntJJ90EVcuzIJMCVrnNDs8pI8YMxUoD1GQ4gw3weOkJzNx6c0OocD7qNxcQeLEQ7NGUseImK9vDkBMRLtrrREhK0nACAbUxtAA7d2RbLjmdiuLY4atbbGksZoprGWt3tA0avMYVq7G/m23jbBrANHpuHRrOm/PpUBKIzhS62jcF1b0bmVdtQtacwgpMjR4uj5qzaVdA3XlV3k2/Tqtvfr6nhkLxCPWyOX+Aa+Fq5w5lGTW2/yHpNvrS6D56E7ex9jHBy9A1w29W6/xbGWxQjpHesS40TP2qQ782N3c4fHUjjpgT59wyF1ybRSJVhqwLowAbfXYp3b+uwl9JMsnD5KFKz+7D2Xww/r1anCshhSQr06j0mpTlrrd/hGSihRQkaLaKEsiliuN2RhCgyT/Zbw/SX0FWkBFDLGpVDm8TlCHEo39IgenUMb7eCAn/kpJBcVZ4zCGH50lbu1ffy8xPov/b420sNvr2b5KPvYkASGmsRu6Y7F3yH21XZdntu22VQCDqZ7Bt3D3UlwsodAYZwfuBwLuV9ClcOMjaeCUpXjxSmkagVceFVKsiwhMMP+rGTcf4nkn+f8H4SafLog3b74O0TkCT42VNQ2pVDaS0rSCNTwOo8jAJvmneZ4sFRTPknobaNY+3DEqo0D63PGD224oDHbBs/2xqEFI3Ax3/sm8nwG0pqmMZD9zBQMgVfCi+CwGC6GAGZyv8RbOhKrVAYBCrax/9U12QiJFIdES12zXF5DOfLFTG2/r80WCGY3Te1WmCNLPmeqeb0DO8F3cAUnUePuPsG38RyHi5Yv/trUbhX/obayUwBxbuFSvj49/DGz296SIh4O2OJvY9SJcuWnEp8LJYJ2pskpMulale1YSfl7NoO59eDSjyNK0wMIrjTohjgfo0TjiFY/0/GjCyCeFnyQFiaoltMpnih/xZX9EOVUtNUfC+dR8AropgtvdYEjaD0EuLi668jXsYTfAFxHmktgcxE6d5Uf9NPzkjDzn7bXqU0Kcu7/eLeV6nNIEJe7uapSrXawgU29BGS1Wo9+nIfsglHRer40L8Ybal5i0dGSRhIqNT/qkWgII4SKjE+jGJNSCK9eE/DI8z5iMYjSy6w6eWyQ51JZgjHEYCsWEssiMYYr5EBcXz3oIJKSn8CrVoqe9B2hFtSRCvJxpRSHnagcoYm79lnkcUvEwoUslgrtyzyQuT+zNMzkzKCKgTcLX4mVU5/KDYuVHLwQXYr3CzMLtxcYHd9TswPkaNVCdzFcLGBxXPAYzFLq62mJoQjNYhXCgqwHu7LFxssU4YGs+eTcyuv535Sd75Wx2lbny3GqnSpaYvazvYLNPhAWSCAWTuCZD1HDpvD+j+oXDVMnkKCDgYFKPLlBNOUIc7n+kqb+CLdQ9AwVkJ/QS7xJJHF9XbUGxq+/5EQ+FJUFfvkApycGqE4Mw9xjJSPuZLidFIDq7B14gXPdWRZ+zVJSdPjK/VQ62lhT/lHgel5brvbqYc9sdty8yzda1C+lUdx3JTKYeVIP5UPsu7mbb1otXLRJjLjLvIp7phhPMUF5utQ/vvUjD07naoDbD7p+PbrJgnUGhzM2/kOGup8ILG6jey4vvV+7Sh3Yzpza4lTfaF+1lEbecMd55jdmgf+d7LPHyzdHCL0dg2wdRbVXpzPeXoTN+aab3vAgONh49umUVlR9w4KVnkROXtSQ0QPEFkXyksr0k91u6e0Ar5MCXnWU2oBdjZaonMCzbCFUPfdGheQs4c7hAjQFnYJ07QrjVhFw5c6hZ0x1MslWv/QmDRmpNZP1a+8JsQho86eiyacn8vKTpr2EmPdJNsbp/SCHmQ907b5J93tbxqPKLS1jCxSUdBnJNtTvijk/MiFaUoLNwet23PpXbxeG3RO/p4mQJ6fQ3LMl6l6mkTqo6SeYeZRHUidHc8+e5z7Qb+U+2OMS/vCnmYY4nSO75tK4fLXquL/LPX2NGOZj8poyAJrLd0FtF3PWKmxQY+dJeGRqrEncznZP1gDPqKaP0nM1DtI9+hncWA1N/CpdJPkchzZ1Sa7t2NjaSQU0YoC0O4nPLJK1oQCffaxLX4TS5mSTNg2ijFnaMvZom7DBpXJ9VL6vgT7mQwPH1diU+meguOIQqESYNH4TZnuoFPLjz28funCrTkuV+0Cr3kRULeatNLDWY0JsRKw6cwH8JR92du1E/oqZRs3CTdIiYTXHQl50NY91j+PTm1EWE4HfI4PFfXkjeUn7ZBt39Mm7DWRPwlPoog0Pl6wVbVXaStlb9SPivHU4itxVa+FWemnhcO0KizuW8pXDKJ+HUegQ+W5q65PvINzjs5WW4bPvvB8GYZnL7+JJmI55LMTJZ5i0VNpMiSE0pJrE/CtNMcvWb4t1B2FdkOmwnSa4nC3i4Orpa9dF6udXLmDANgepaXYUiMYBp8Q+gGPYfybscyWQBAfxNge25kmiTbSlOYavztcSz7Wvkw5cE3BP8osg2AHEf+CoD4HjWk/N+xfyq2KGIESAcDiYiFmNw+NnhRAJreWs/ZncaTauoe8sTeZ+CyHKxibOJf1+qbasPr+2Y9WriOn/iThSaFe/np67yA65r7FTuyVK8ii/M5fnlpIsUybmnEfIxU3Sv4JPzLHgeYDccgmMjBUVzSO2gYhwhG+0eFUsuMmAqCjtvdIWSpVpo8cRYhgKM0WAudBwEZjMPCX3XAEBVJ12NSHcMq0eEKQ3FgSnaPJGhHhVmiLhjfaJdQsw3oEYlG9deJfkCH7p7LhXAASMc3CcPM9Ck/Siggwsb72WQiJqibqpK+NTXvKl53k3z/rBjDXPlF/lc0OeWEeEUK7m6NyruRo3KpTvapDEQngGnA0edDx/qKlp+I+OgXdz+pG6emJgFwVmJX2t4YHXW4e4ECVDXX2jUGoWSilW19MyNUE1QnN1qasPCoba6ljcRzQZlCtdfranC8Qm2/EMIVHZSNRLycFbdJa1krmvv7jyBK8+vfzpata+mpiSEpwSRnv6Nes8eENZzcoGlKS5BIWHO8OEwXb8UBUKU5mIsDqeYK3hOuld0NhrgpAU91Y48+c4IYg6kWL2ntgpp/fswSw+qt+nMZ37rzXn8mORpPx5/9vZn46aqNK9EXlu/O8/4mP2oVFEM/WlOSLUrgrTjikaquR6fBXilKGMYRvK9nNj33GEUkJ7zUlTUGMOeJp36ycmOaUB9KalwWE6ac8go8xrzpl9MCRFuIOT/TW3TU1Ze9HvPq2QUquxKkrt84wUPsNfDFbEprzgOEKC7mA25zSKopETalokPiRZFqGpGabTBSxhlM0LWRpAgfGcd9UYz4+n2N69WACUYamKKKImogvapguwLyuagUbN7LGEuKAeDHs6oHT7Exx5p2/5eKu2O8h3a+JDUuSRsk1d67eEENVU9Nzmjt01F1Q8m0NhgIk24pOgpPKm0j/mP4wsDKRF7h3nY2edn/stHfBi+ZibxeuM23PtWJEKyZ0Rn3Hb/dbtFY2/L1wrojm/SsSKLwiHHTAy9ZJPBp2knEtXKy1cuAGTcudhW1phldQncBYfHgSxxkcBZx6SXExw0VNtcX2SpWN8cyYG+YUhzSFYDConyF5PpAvfk/Y5CuTvYE5Lb1AfHmPp7rSm8+8oiUfG6FKVTUFkMkbHYh2FyNvjcflhlkD/HKMMS5vSL9JpfObALOeyaAJizIkG6BTiiRM//ChjULxANpdgoRcKPyxhR2qk1B6sDankmjNp6RP2Trx4vcgsJJdLpaxjxQbRU7/W7PWYyoeEem13x6LDatdohKuBBjAO7kHFcM0L4hMc9HxPhIYaQ+IfXJsM2IHcGGUZ/2akM9Kbojj0gQsdR9fERtSxSkN6Pba8IOkpKih6a1gO78JUbLghDHB1S/QWzoV00YfPFbK5prUsh8DlkcieMimZcepcvhqGkjyXzD7yXpRQ4U+893T1MmXjUbHgSuWSkyEk6XadredfKPQrBcf8gLFCPe/tN224fqksSA5IXMOfHdR6/AJkwG9ilOheBqb7ZKqZRKj3lpRpqTDnTvFYC1Ai4OPzjO4NLR2CveOnDkb2fXu00KpsjdNhF08gu7GEoFawqaknlqaXu0m5lIJXcbIfnSkca7T0jKyNcjOWLndA6TIH+EfRJRjIPvIX3JHlr0KmNgphUybjxdO8m9bhDQW8yzgeB6u57NNmwsnu1mNulj7LD19yLY7UOgx8qV5OSwLkVX9beKDztfaUe8LDIGKKm5zwnOvWCTJgNIzpss4eAn4hI2AJ11/9SvsbnqXMh3L5RWhNPh9hSFaWQ5PwxWryjbqjuXEudvTEoJRG67jYXbmhvKqBRh9UsB0A8JWbEACgwcGCIkOKiIFUIMx9bhTgE0BCT2MTr2q4P4rph8PDXCqngI4bY8LZjElgB8lAhzj0KBuEt2+D0dDrAkgfF6RBHiXrqlQzRjRHaGVavbHn3yLYESAbBJUi1/IJ6YNFcLMFtg8Am9N749Dm8z+TAR74TPY5o0W3a175nDt+jOYfipjKdBZTxm0+Mqj70Sc85bme52mv5y7eQ7goo42Zgs5KhJVDQEJKTrdev56vJoRDQtYFNz32mlXP/yW+4Zs/957fz2u9V2/gTE5w8lOfx2mv+y5eH/CjjpmctxX4DOx8IlIyChwcTb+UydI0s4+pZ6qZ1fZiu63I1JRFjC9w98q//h/Lvc9l/TUrF5cbmVPllDlZjpnDB2eDI8Hh3IZGAfD7Wd689rfp905AADYyFcA0AAsA7AaIXIAYNSAzO5hM+FtDVxkSrl8vKEBFgkQDOjDACvEnQNayMdlg5adxxptgokkmG+OoueaZb4GFFuEyWWyJpZZZzh7PCisVK1GqDJ+DSlWq1dhoE4Fam22x1Tbb7bRDnXoNdsXGVrvtsdc+B+x30CGHw8MRxxx3wkmnNGqKhY1OO+uMc867oFlLdIQGrdq069AZE2t16dbTCDjqNTYGYgU3IyFEhAC33HbHXff0RUZwIPSv+x7o98hDIuLs4eljwVAQKhJCKGGEI8cJBUqECODjgBgREqTolSIVAgAIwK/94kGJld9bieGOCgAePF0X9Kt+u1qGc39UJ9NNAWCA5soaWFemkdZsY0Uuz5IVElHnLVqlhKgQKZmBQ3ylKBMoT7lwp6VJ1yYsKnwynZKCUpbNckwnoDdWIS2RNHvUqYsKd3PSJIdCzFLn2U67CHuY3Y511vqjTzJwDu7pD/KJFM4a0Jp+cVshOZTT5KfVhVA5PVtpF+54sBaQlPdnLy8DACkPLA0hhHagVT5qm90VqKPZC5VeyFePm6oeVIExyF5lKj1mUEDLzz5UHM1o5qAsFYKt+NZCGYJ+yjGn5cTdCD5wylYKe7uztyK/rkDdGHFFCs0Ne7Kq8qLx7acVVK9+R7hQKRByWP6/uoyNqTwhbSwh4h/s7CQHjWREdBAMukW8ON/btnN16P3hkRnh2CqyI87Ix4GG8+eH2y1dep0FUOIvW4C/2cMOSIQAiJEwDBAcmfLNA5cjgIN5l6OAwqLLMQBYEhkH1xAP9m42As7ahZhsirmmGavIGDOI7NM9enDjTndYJqTCRHLN5UCjMfW2Cb1vd/ytPOPNiIfNNImaIQhBiKnapttYCmymwDKLAvOpJbO4VjJ73+zNx5psEhF3m1vngwEMb1gFHgYAAAA=) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: normal; - unicode-range: U+1B00-218F; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: normal; - unicode-range: U+2190-21FF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAUAAAsAAAAAB0AAAAS1AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYgTGyAcKgZgADQBNgIkAxgEBgWBdAcgG3wGEZWcfyE+EtMN9vKA5Bc0yUoz5//3zdWt7i7MMhk89Jh5YNQyuHYEC0QOBNNZU+uBFf/fX3vNFsIYcE02IK5Pv98Efw+vn0T3zFEboYh4FE8cSTQSMomQ8Ug0TyqZBNtCn6ltmWJarrMX4mswKBIYY7H7bxbmnSgqLFpx8yIZ/cA9xgIYCyNgLJKMxbKRoIjYCNlyHxrXrZvx+abPHxHcKkZSgdCXUgHlHqe3oz//PyeO3o9FFGNhscnzV2zcsf9YxvXRZ1+7WVpaVOU31l5RbOUBkheyMMmlLEZKVK6tsKIwZ+2aLRsVZRNls3HL+pRnzcrVfUmk4lcW376y5PCf7C3gZLvzqq35t/M+nUD5ywykaFPZH9GRJYNONVRVTftcgLGWUIRLYJCwSaGn8CfbxwbYj5JJypDaol6kJ31kizO5PqQxs+bUnM4k56Br0Gkht7C9/jE3vSeO0BJupOuTZ4LoOC298SmP8tF9cfSWHk8L6T4zLaXjfPKCbpwNpiXiyMevCxu5WT2DTtfw9clOp6Y504RNeHjUi8YuUazT0k/jTC76zsgym27QcbGUT545YKQHiSNiCTe5Xk8nN9n4YN9gb19Sn6a9FuFRvaI6Ko0/qhi9pcv0xqB5ibQ4a3Jf7+BgXxrZyJ3+ulgijnDj7IHJC8FiKR0/dYNH/UqVVt1FR+JMrtdjiDg0F2uofsDBPCq2p+4WkcQ4/Rc0CYQ8BEJHaTsf6B/qSenV+i0mVZygWm+cwgtQpJC4kChANpOaHAGRNzFs+v1rfGz+9AbNpvl8qi5YSM0pQrJV3ORaf29oWT1DA/2Ila5ZzAIHJrSkNB8rKfm9uOm9AUf9kDPZ6dI0R1rUF8YxRkHGSZlyab65jFykC/2tt8U39C3Zy7vHxdiD3N3scyc7HF63O6211dvU1OhvqE96WIxzBzb42zglGy8bF8z+Hn93V1Knt7OlzRJwO/wO1v8WY2Xde8n+gPhGfPvWRdJJLxOucWcdzTC2mB9UWrFUcwkjRWoEg6dhL7EO5IEOXyC5v98fCKS1t/s73O/Y4+22lNA4D/tSwcTNDd76pqRGf3Nbi8Ud6Pf2M2uo+5MN48JeTt/Qt5feFrrQHyTX3j4epQ1O/Da0a0hUDgWJWx3B2qDxh49Ezg2FiEJfny9UD/swXA8EAi9OzIuI+LCvvSvQ0f1QROTE4lmpAO8ygwEIQwoLtv/g8TMIgQIAMzMAABD2QotJtUXl3ywvRAIQIlGRnmMB8EkROUL6DnCpsewhrM059FDtivfmBOYMyI9OE1B7WOhuBbs2gbRVjgCgUgBIAGQAs8hgClGgQgUw35zPzEgw+x8lEC2E3C71ywOzS65Hb/83RFaeA4Bfaz/+dHJy/dvvzkxNU+hLyj2ADIkxRf54muAPswIYM39VAEN/eyZ1I0OSc9Y7J0auUtmylYIVwgEE4xIZTAkFUAuzXAYFtVwJEhxcGYCrXIFxOZK49EEsm+9TpFiNUjmyZCunehyqtVZbY3NNM7KRV3UL6lFZbUCOfLVsvkm35SmXrUKhlVR75ANKHNWXEbR3JuEdbaX12qwcQKftHPKwMjmKFFKtsdJqwZQBMWaGrQUAAAA=) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: normal; - unicode-range: U+F8FF-10FFFF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJ0AAsAAAAAA/wAAAIrAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYIRGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzYDQC4D7AZ5j5pUbZm4E4qID3FTshVsW6OPY3WC/+/H2rnvv11ENDEkTQxJq+WNZLJJVAvFm3aTBlvR0kSNfr+2Z6qhKJBM0ol5Y6ja8JLIZEphKPV9HOFkV4XMJnQRxziiAWnTbbybh5rcbQEVaSD4FH+CudcDRKD1sl994/9u8i2QA733nQ5oNDZoniWWYBj3EluDplDkEWft9AMzuudgMIgSdgg3JGVZ6nd2Bv1B6OwinfywmQofJcRH0ykzg9Iyv2VCRnk43/6X4PXUhMnvZ0DScRl4PMKMu/dGkVLCpfSOfGPKU4mFo9Fou1nWW8n6duj0Rd30ilt5v1vpvG4gDg9rRU4mn5ajexpYLbO6SVMBJFn6yMPnuxP+npv9XR6Oexf1IXJq7ndftzNJD8lJ+eLN4tGnQyi82LheWVxa3UxN+/QqTSCykoeAZEqVU/vW9jG4OMD/A8AQVapZPXFvadqHlofodtvXIiw8pNo9iKFmtUk8TuFR5f/AKby9MHb+CBB2lNQMrABYE0KgkgMgQNB3by5Ifm74PoDHdQc8nr2e/PsjvPM4CBgAgnD1R3CR3f/+5wka5ppslUPoBgEJxDCQJwFDoIFwhsAwxiAAE0VnpRGKc8JoieMZA0MrYy0NTVNWe6wa1KnXOAqs47ZKPVasujW1dOXLrh8VHVNNM31hlVp0dVnrDCe6X0Oolp+rqQoHNO6f6u1jEy0DfVb1atQJDyDxv8QBAA==) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: italic; - unicode-range: U+0000-007F; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: italic; - unicode-range: U+0080-1AFF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: italic; - unicode-range: U+1B00-218F; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAuQAAsAAAAAEpQAAAtFAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADZ1BGyAcKgZgAIEsATYCJANCBAYFgXQHIBvMEVGUT1YA4CMhN8V6Y+ESKqtYNa/7eX5uf9+293ggTtzzIX/TbebHn4BRAUYDxhjRyoJRKYzqIY7ogcOoxgqw+zy8U3nedu8PJ7CLCoMApP9/P1fvRS2NKBb1dPNOiib6SGJvA08mSSVzFklmGgnZQ/uJTCXRljPU5TIVcbsZEPJOW1R2QCABDxEEYTdtXXDA/JDgkH/fa91LIT/9dG4Q4rYt4g6Pc+RzDoJEWx7uacuXGlHiG/pbr19Zh60VDsQVfRGyvvjjKglNPU6PBxMmi+BRT0KCoGyYPv0d/xs2avyctNIq1yClz7on1/XVevkpPynYb3mZW7tOoVYpl7Pq2u+RIK1cLhHo5/6FUl2vPkU4/+cE5XTIdHdUuA95tM/pS7853XvX2y1QEfBLCVz/IQNH735hf4COk9Ze8lZE7i7LihBBpCLEA1BaB5OBBjX4gRY0rNAgFej4joAgBWITQqWISXwizQrJCxGIh9xFy3WZYAkv4g1PxzvDF/A9+DH88wKNoE3wmhRcSjKebKcKrQroaOtZ1jdslDY3us3utt/W2bbR9qtQ/6DL9gFRag8qqvABGTzrFvwDA09DCcx4emouHuK/yTc1RIaDgrFVP29J/GgSVFTxAxJ3p4B5shumwkAxeAz/hgfi+H/wdrwE222fulMpxbaUEEraifecgs8dgXK2Ul+du0Vq8Uil1PqwZK2U8/A0Rw0H/C476GAtMJ0Duc4uk1YEGSDCf/Nyxsgc4bpzz9jMsrSSlPy6E+YbBaWrNzsEqyf9GTgvOT41Nj2a9k0t1TVK9rbW7c+RCbk0qBNBBSzHDIiYI1DBGdj9OpIxNlbd/d54hk6nsguyCjOK6Nbs6M1BEg/f0NUpsrSozJicRJo5ErrIe3JCNC2Mgp4DNCIYB7F4OAxn1sI43IPdF0oybjVlpy60HaN3VlfsljE529WVipQUfXqqNCM1RZ8iUWxQK6KEDIjXvjZ4mdes+BhntegRMBKP4AbhKM4kxsjkwCPuLOuXadA1SQqKM7ILZHtaG/Zmy5Slw8IXTvZxpdPT8GWXKJI0GzIUNHMET7APPIewy6gVgYgbhB/ByLw5lTkCByxE59RUnLhpPk1nAIMzJdvzjDXp2+mW9IjNgZKE2PTUBJmnb7BHqqmi8AT7CMxs5G07e+hh7V1GvK9TxBjhAhfBMpOGWyLIZxRjfMTxrOBIlXWSuMvKTDKDN+nMFYzeOA/uT0WNJMMTy5lOWDCqmKEOApWUONCeuQ0COzLBgvgMmT2AxfnjBZFnhY3gSkDEhHz6CrvA5H/mtMnUcPibXF39/F2lhggg9pXeF5oFeoVC/H52faV6x/bKyh26j1qhUiukeJG93AbOjkMYcwJmHwYd+ykS3NeuKaylHL8+4cVthjekZcvPIezTmzde3FxxYJzT9Fn/jz+07JbshEARUb69ICMvI0+WZ3W5oK72qGTX5kjFvPD5E2Q3uPWhSnvpDDvOGc7c3e53OCHBcj36pxRe5RYT4AolfK4/2LG+e9ce05zfcfnUsf17/U87zJqni8E2WEBH6JISE8SJeYklkdIObF0ae3AeHRxX/iegucpePldeDDRYlxsK8zPzswpS8mM3DwNBdMmqcytdHVo9di2pnOk1d+GS1R608An3L4EDu8R8Tsols5bv+OfW1dx3jiSFUfdgfvuHdtDcI15CFotH5N0bCP0kIIKeD96aZH+Xk67hYxaPlrisvgjdUmUQRhnaSRxHZTbltOS10hAhe4BwCmxazwP6LLmyf/WcQtmbKvJFpNMdTEqw0xDsiF3cZcuoyOnkISrJL9UvLYD26NYuhJdcb/0RERP3oCuABYJaaulNnqOOcsz32zLfHug4jMeQR6m3MMEDP5byO2e5ueRMynvK2tmes7QHgm9pOug5FDMpHTdZCaO2ww5zB3zAS7XgpxXBtX36PdB7/7o9zD3EuUWxd05vPZgny8jW52SLt21ocK+Q7iyvb83ZRjN347IzsrLFe9Y1z53s7uPmYwxqjpIy9+Pmq5TenuJVh3xvSC0DIJk1B3BN8Ht9I2nxgr5stuehvJb8si1/ME/QetxCHqMOwAdyMaXa6KNZHk8DWC6zB3W1YaFiH4XKdeWG0tqWHa2nKqRLPrAmc91uKXM3zlQVoQrwD3bXyrwMusoqsR6rP94JSGvaBlY7RdBycv1Jb2VyWIxw7QqbKWeFTJTa+MtNxpJDsJQTsP6e5ZP2tvPmjY3+RTLmp9u+ivqtZvHR+aY5a92jN+ikdf6ksayhcLvEZEpSKpRBs2VqKsWLrKWYX2tPNejWuyujFniG5zeqZX4aUmls0ZyXOOJU1kBTWOrwTxJu4OGsypWYIVW6B9xMYItfbvTV3SDQ3tp5ZncTbzoe0dn15yQTjbI/5Yp/w9fv70jHw9NPzq3ZtUAZrIuMkVW/JmEElbubxP7UWjz9UCZpKG0obpbsqlEvn+0V4uYVWlytkin9yEDTtqgdkuE4Xlmz/W0KYxuu965jaj3eNrW+bqp+bebDLdwStvT11WuXr5SUOGTCF8kvKCwsM5Qacnc54HoqaS0J3pROG66NicEktvFyjZtCL8Yrj9oDY8LidrTlshaGscxrTZw1/C2Eem6MHkaJmLiv9vepCy/3v90GKHtA8dCqfvQ9ipnUCYsewMJmUNAjqY141Ercdy4eTGs+xT3+Ih5BhY4PXTgKO9PJJzadOCl+nPes7J10JBXzb4qLi5iJ65c3oGCAlCO6HNkzfacLgf5l2HDuUsMpQyIWvD32uW/XoM+ub+n9anAfY4uP4Nl9gBXXsw2U9AgqCI8ajh/NxAOT25LNyc0asEo+kHGQHk4FTQwc9w8ekwADD8KjRhjl/9OYOCVW+FiYQryI5lK7fuxadreXlzNQylO4xKrEQ4YSAGA0N5CtXA4VIPO9SdqgF2z94ZQur+spQEznIlnQYpn5MPaCEX07+3+kpnyOV2Bxe1Q7PLojYq5w5faFV0j8i2LMJ6/ueFj9jIY5VFk7iWdTWVsy0UEmvPACPKh7GXXQ45tk34GwtVWyE3vJ02ELtjlL8EQf3B/LYmX/XyBO9IlK02zSbgqnMZoc57VrRvtOnjMubqyDUM/1w33bdQ/h0EMR43Zk8GB3YzA5/ngC+YgyAQuGyMSyRFOM2E6l1ySsXZThoQOLOAmHJoXPVPnaLbOo/hYzuZwSRpV13TdMNGCNgcLrc636tXAdelhmoHGwvlhv3W6zp1t7Dtxg11xb2z0lhcU5uS223bsce/VFiMxnEYEQskF9SBHTZsxbgmgk+EB7Ya5+1AZzxONOGLhOFYwcfCLKKuBtJXIQyQ3wbXlXELRqZKIGuWQZAmvioHxMFD9x/Tz59b8AIZaDsJ6EUH6tzBvDt/WWp+nJ65Dv2Yt5E72iAJVEStlJDyaUJst7ycVyqby/fLDcST5GPk1eIzf1699vcD/P3NLSL2fkDvI+cnDyg6NFq4VWM7XzQ+fbzjedrztfdHZ03uy8iHR6d3zquNexFxFICiw5YO+rDwmxP1w8Zf6RAGJT1tHHeqzpMe4bzReYEULoXsTJ03zl+INHfjv++m59UTAB8RAf8exN8U/+ApTRbj4kftc4SVv9fgAIwStAwvZ6FP7wIzaoQZUYDZokeY4uO/ud/O2I5Bb5F+NXmtvnmGU45GZTgpAVWtVpJwTWCKEI13UNAglQhAYP8VCMBh8hFFdV4C+eRGI3byrM4acKEUpnIz8+fKnINZJz4cTZqMWsOe0TufV0LNKNr3/vwJo9kIVCAJWcqAX7r4fJTj7Kx+odJvFUclR6aroIL/+hpNjAZeSOrrr9hAgm5+ztVBE+Qr+5fqBy5AIAAAA=) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: italic; - unicode-range: U+2190-21FF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAATcAAsAAAAABxwAAASRAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYdvGyAcKgZgADQBNgIkAxgEBgWBdAcgG1gGEVWcQwE+ElO4G3pA8hc0hxm3NGG6/8up/ZE0IXYD5JQJQigjvFYGThf5VABWk9PS4bI3/ONOAHFwCxNLNbXr539uoRZZW+ctNHr4gnzB5NNGgyUgiUYiIVpoeKSemIV0+UJPp2JbqHYhnb3wk1sQkAQghOQtP1J7bkNdbd2Ed62zY/jbl7IRwEZmARslsNEiK5EsWQLPzxKrPgHLtGmDHo/hs7NS+Xn+USVA+usP7yneyn36YVUB3suHHEIy8suHT5i5YPm6rYc++unqkcbGujbHjdmW+gr7CLfIUzhDQQrTuo/XttSemTplzszcNcs1Oz5nh0pMmTh5MRQqbf/oY/vHXP9DXwMOnc+b3Hz+2LnfThD+zwwgPdH0l+igeGDa0qWNYv8HACGOdMgUgIAA05z1MbwnI0g7uUL+FqYIF3LQo5Iv2a/iQ+zRyAbZYCgzxI1xQxXa+fHrj/kaHEPZFrVvewqux7GHn9KcT1rZn20Eb6kijsRPNDgW19O+3SrbkYpj+JrH1/lxtNO4wRg3lBsMsmyo5se5i7plv6viO3ZK0b+ej61sX8G2pFQyH3N9SwqPp/WReDhSFpGXpCqD4FiOzz1xI67ZBFu1PBKOk0k143po33Kdj+FrKNuxom93Kh+L6zcddktLJNPE62GS9LJRTphiemktn1+bjYTi/ymyysn1LXgjzqexaCLksj8sR8mGSRzehAQhUE4EcgaFUOeSv1QH/ccea54exiE4nPZ3pfKMbELUzfwI1WZhz8mhRCwqbcaDKQ0n2pIUZfIYgfhvojG9KWEoNxhlWV+d84KtI5jCNop4FodrmtCIKldv3eZv8C3qmoO9vOdzard57OV6vdtur3Y63VarxWs2lX3Je6kNT/iLbRLZj2y3xhvyBgNlfrffcbFKseu9et3r8p6m4FLUfcbf8Le39qCKahM39hoGxz+VrWyO5vOc8s1kLwlSCdcQ5qWoeyhVfB6lPBr1Kkr1pUtenwM7Q+5gVQP2Us9fKRWpxuw2WcssXttFR5Vdibqj2gTnP2ju5bpmfINv997mKlc/R+PSCM2R48nfE4sSvDWRwo/6UuU4+9ODOxNpvNYT8aSrGdcyVUVRkuuysq5FQxHF92NWdtI2uqgSgAY0CABkQIUqZPnK9dshDSQAgMFBAADACYw0mrBYzvkjzbVQwrRDrST8TBSgcpCYJbwDsHe15AuYOjthYdfuBVNNaip+PYBgECR9cW4PaCDMFbMa7CYVKrtIirIsEFmSQGuXbKRc08HBypbsnw0cCimurtT2sysP5c7/L02UfgEA+K3z8VP5dNPte4OjB/7f19JCEEAEQbNJfDyAvBmbGb7BLxA6PviHt3+eCEHIgTI7TbdTnrMaHXZaI9gMMgEgFfYSgUjpANAJzAoBCToVAQTQKyIAGEclnkKhjJGn6Dx9mTr1OjQ645TTmml9C1pTTTbF7Cs0oXVJ6yhXaG2uIJ+fN131vD7HnNPstBa1JtJa4jxjyFbehNATsv0Eba1Kx01c2g2ZnfCjmpxRp5bWFBNNTqsiwCCr53WbagEAAAA=) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: normal; - font-style: italic; - unicode-range: U+F8FF-10FFFF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJwAAsAAAAAA/gAAAInAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYIOGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzMDIL4Mb4gcpY8lI6bRFpuvq4/OWEUk/8G00RY3bzZ45QTP/4/7fp977/hhqDRrEowk18nij7AjlZhdyoMv5jaxZE2Bj3goRCo0jjQxTYTacALb/XAD3u4CDzHDKgCagaf4WPsskuLqgQ3PZuX/n3qBs2kmff9/N/kG2b0PAne7CZpHlGAY9xJq09hCASUSoGTuSs0f2zl4eIgSJghGISKxbXaRXsuiPBy17rvw/Wa3uNuCe5R7qd2i2WdRPsGis68g+3ZpyKuftUT4Ml8LiPrqLUz7F5d4lxWHEbH1TwVznkJxQVFRUcPp5HpB22zRiZu6iSW3/FDmps8biI7jmrPj9tdyRFd98BQsNBZBEBFl2zjn+X2+8fxv6Xy4zzkaIU9y/93vGj4f6Sv2n94f9TP6mJ9Hjhzx9Fp4eubk6SNHH1qsnnViFoQdT0KAaDJlk7aOyw1hMAChEACF5cmnyrGLc12BiEREu1GP5QhhGG1RP8C4Wq3coCSGCSj319jF//XtoAt9W6KaQI4DVI22ALkMAAIIJP+TmbHrYDBPAH5p8KtXb689cwMKjQJAQH8K7tVQaL38SkDoDwAARJ3ABkOIABGM44iJAtYQEwTDGigUW6CBnayhJWGkZwXhRfafy2pu4jDqwF25ShQpVrUSDJE05ZqfwVAUZ+7L7O+kXdQXR7maswwqDVrA81Xk1w6+d1Rg+3GHm9q4LECxAkXRaxpCbqELSggA) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: bold; - font-style: normal; - unicode-range: U+0000-007F; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: bold; - font-style: normal; - unicode-range: U+0080-1AFF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: bold; - font-style: normal; - unicode-range: U+1B00-218F; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAsQAAsAAAAAEVAAAArGAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADZp/GyAcKgZgAIEsATYCJANCBAYFgXQHIBuKEACO0xV3IEmSe0c05/9ekrsEyk8lPUoDXK5mnpohngqaBNGKEVwimNYU07a0Dv5E6RObSze/5SH/9u5+lMBf4FlUW4YJhFESSNAWUeABBtb1+7W6D/coHi000caFQieU++/MG3g+tYRpKCbNZPo/krili6RKyB4akRoZMjmgIZ2t37uCGbUBgiAREYR4s6Pn2T1enl5znmmFY0+5mZuCuKkWiJvG46bzubECPNqCz2jRpX7RV40mYMNCGIkLbREy2z3agpeE/JGt88H+N42B3tFIHARlLrGZOH3u4uVrdt5I1e477+ru+My6/v4uCtePCRYeFfr+js4qpetRX3P/L5Hg6XrU7ZyC9W1SGdedChfMnU+qAJCdmu58asZDffgbv+G77vrKzzmf/ZkEjL/HgMgu9Pv3czBkLqsdWFaMNkJEXEPIz75hYAOIQAUK8Ad1mHcgJQT+HmcDOSM2hNIRI5+4IURsIgLPQUqXPfWZWE5k8kieNy+d18X7i7+O3y8wEywS7Ba8I8eR9VSJ8Jpoueiu2SazAnMr82DzTyPUI+6INX1Giz7i/u8c9Tv/LmdN4znNc+qOMxPnQNjPlGLG73AcfN9/++n39/OxFz4+e5qbLHV+Vlj/tOVnmC2FdGwBk/BqKjbH8smlM2JI7Cd+4rz4XD3k0FqNPqmQMblFUGqN8qIXw7lRYkjrIoyj4QfaZFzOGSmxsdx/DGSADMtgoUQrqYEM7h3tonO/U12lv627rAtJZ9Ka9e/i0vfkWKnUB9iwnaLrVyIvXZKKjbrS/pdhIUdmzv4yl0BLtG0BWm3Dv2kd42MSohKSpNWqKkdXN6WTKkGVEcJIasL3KTdcCRWJ8UIo2yUMgP3YBmzOIwBb0g1epERryHzxTWF7fFJsSnS66FFoyRF7Xz/7wNiQxAhtqNrvsfVSmKjfWhvhI14IKdRVB37m3tKuwq9Sd8dNG+qW2uIXn747Qq0+OCEULssvSvEqy6xQIY5irIMutWYGQBtL4CNuBKYImNGhF+KCddp6t8TEKHWVg6u7R3qo494xwKssJdrsQ4rxYrjbnBeekHdu/X8UlAnq+nOxWEOuxqjFDwfGQPX3kipuNPeKlsjBaQp2wrKZIDP5/Mn5wJRh4aJtahmAvQPESMIHlvOhQYbXTDArHHv8CR7SX/EUWLOjehnVWFJi8ITjBKgWxLfsoneWKzray8vV3IryXbs8FLsYcQjYEu/A5olkP1HkY70hvZa2M/jdv2cwiGI/g/0BrsHgvZZ2+nj3+/oIpcHO3s+DM/N6cM/D3m52cVgD7HhGFL6GaV16z975XKVxC23ywcNvTnG+8Ik07fz/ZHqwvfOboT3Nixfv3SeP07pzSBbdRZ8KMDQ3ZusePMpRyvcH+J2WRXK7M+dvWrxonzxY39zkJw9QGkC96QM9VK3jIsptOwevUtzOORCwG6L53Bawojd3e95U3dfdK7r5qseu02rrnjXLDiyvX9q99OVe0Yr8fwTIa6dftnQP1g8e6F8zsLXVbofVq1VFLjp7lZ2ny+bVIvEAt5zI44DP9XNRtKkRf3myn2vkhKQ4pAf2dP3eBeoe4s0v/D6IpPGSkp+mAWMNlmD23Z+5MkxkkPKI7YfXWa861ASEUgbnqYQ+EodTkZmazCxpqiY1KoWBgATot1d/CmhtY3evdUfN8Z3Zsn9LyGHPyd2YsMbSKdgc7zwnO0pdXk4+oSLDNGFh0hBNSFQI40BdXkGK4SdunKaGAGnbv21g18Y3GLX0jv9Mo95xo37w/S2gTtiOZWQL1Qy2JK55zx1YLfRc7b7Z77TPA7dX57pF4pAa0D15AaDxHwPsc7Budm4E8ya3Fkl3BOcXQvc33a675V123CCT9KDyM6esUvrsZNWWTXaKQ27aM5VqRjIcccLPS+EiPdzo1M2YbMGB5so54mi1qRh/IRO57fTd+NLEnFQRzn0OsEsY4HtGfeSCiNtjek0/DSr28ZV6nws+ftw7syCvMP9xFiPpQfJfSOflZ1eUGYL8lAFKtyDGOVulz5X2V+N13T/PfqvT6mtdu15rdU6ftbl/rsmK2c8Jt3Nyeuh5VetNv1KPLJnEpC3Lyi4uk7Zsrd5y+kTguUCm2pU0ZOQnFlrfqrjkfsZLdVB2/mgxJcFsU4naWeF14dRpdUqJt8z+LOlQXK1qt16BL+8FCg7hGTS2GvDOLCP5XJMNu6CS1zeAwL8Y5Hhh3YOGtULzokDkBPLcwSEbxJ1LJ7ee7u6qfdctr9/irb4UESpL7CBhcQNWUhKj3B/LyTaq2JVkpualF1nfz/M9dPys6oTCKyXfS+bkSCru3Q28Z43H4sP0IyeUg5PzTFsORvNoWEhLwAGbkwzj5TMAK3FE45iPb+EALJkRfumh+8radc5I/lN8o3q54HqjJQ2nqGD1pYtB/vu37N0YOUn0looabuqq78zR5+oz9aIaXCWMNFXRjxy5co53tJrEa0zZB0nb/lP2VjfIjRzkwzTL76gakOo7sjpzFhatrJwt+o56BvInIAdX6UwKz1VjqQcex5z9Sfku+DfRLCpwus96j7miszBOBVKYK51FYdcHWO5MwkC5cTx9VozPmvvJ8HFLjvmFy8QazqSB5WPuD3KTBiVVMM7ye6p6+D6MMPyum3YPj6icKfqeknjXwIG7nAScOzPfgmDc7I5nMEdghup96Fu9h5D1fusO42kiBUwPhWZYIJ1BSaqw8x2TZIQDnQV8nn/ZkMqxL7d3/NffGsfREA+TjwzhcLyKxNzue+PPrX45YP7VxsIwPLmi7fsD66D2cyB/5H5Y/5NLP3T+QMD+T3w4AK30xW3KjYdXihZzJ/rw38LHH/Qfkt6IYDsV30/iHVS0JjpamqZJi0xjYHO8314qRjNGGhmviY3z3UOqJilB+mP5neYHT73tSpiWJrLdY8fNpdZ41JbVeEKgJiAqUDaVurKcBD4VGhUSLJ0QgeGvdTqe8ZPjMJQNjwGzYUka2MIyGi8bFgnhqlBy7EcctvwpVNtK+4SroRSGh017Etdm0u2fJxSHZBh7M9dlYnUmhZ3ihVFr4fo1cCRThD01qRqzLvP6EV1x5EZvGSdZWNSnxibGxSflWfzPOH2sLULkQSMCQuZsMIh56+5DSETwceyOiJEnaqrpeFMTL45KT2SVE6FCQT6mYkT6UxQL3kuE0ImJiQstjCOI5BABsSRIWyMm2PELv4BMxL8Js/UIJUYg3sC34LFtcngG8TljfTzC+gukFTFGxR5MXK+wY1kpy7AT2ansfHYlu5nNY8smTJwwdYJdby2ZlZWwVqwNS3AetMLZ4HDyuQ78PjA88OvALwM/DvQPvBvoRGjArf/P/p7+B4jA4FmsWNuyYU1qfEqEDIMgdpuy/DIT7Ueu/lfEF1QhhFBPUGOzf+ULT2q/fv3yySxG8AER+Hj7twX/+QIoqpXncPtqIkwNZH1EJiG5WU6zCgr9gb5DFXQaRa1vCO1i7b5v0XiWrEw12SRfxTKzzHEO2gfJIRZCQifwEY0ZQigICscQBIJieHjCYvgQimgqyFdBkqbxqDKH38SLt0C+FNx5UGIVYy003wLLO0kIu1KWk0CrK+dB4ZwguLYJOTtLGS9V8TTXwgaco2qtPL8YjqsfuXLUpsjFXIrCJUfEE6mJFbx4Yi3gzRcBfKgAX7PQQggA) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: bold; - font-style: normal; - unicode-range: U+2190-21FF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAUwAAsAAAAAB1AAAATmAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYgiGyAcKgZgADQBNgIkAxgEBgWBdAcgG4sGAI7TpTZgEnN0/bdZ/WbmEV2iJA6zYsqmkhMnIk5BTAcihkUMkpy4sKwZOZW/86+rntyLICrXkQ0QR08+dWrm0gQ092BRC9DqXtSrFmhMLHCpwL2epzzWACpGhKqRXuEpp/dUeAUCAgeEEGVSsaH2AaPBuPtMK1N7Z4K8GeQtgSBv5eRtvBwusJBAXv0Y9B08uGa1Cq8P9LGwZ+MB/BzxAMI3FN8NuvJ/Wyh+GAJKQvxDYjftPnI66b6Uwkdeeru4ocHYGjOaaq4brORYs5kiqzHz43HWWGZoNlQf2H/8SMsd5Y5Fjq9N5ffv2QcdWSrnbSvN237wz3wHOHNnXr9OX1r74QSeH2ZgOI82/g8dsxCCYs+IPs2/AICQIT8I4IAAB1odPoJ/yf3kNfI7t4Mr5p5UvokvJnrtWBYaZvbgd6pOqUvqipGme1xmdViRfcQxYrfgMKuIct9ip1BD5VM/LmsVqEUx6zZVWvExKy6G4kfesCLcgU4ViqilYY8tJ/4on/VBDTt1y82q0EpdZpOrO7ZL6uqUNKyMWanyTVnL6ry441JomBkvyeUqPMNEuqw9Lp9SsJNM474Lh7CChhXZbQ673e7rmpmedsbMdTk7nWpmwTKqHG+Rf2sl+M6PPL69oOp0OjvnYl2zrrkZDVah5S4307BTVE44vpzkw0TU3plFld9ji85rxnvIW/ilSjJJZilGmu6d7VE7hh0jjhF8gJ2Jct9k3L8UryoYt9V9Nz6AZxx2dKEjenZ6esoZ4zTNdM+qWTomZicht5Wy6wrk/s1epEpWu7RgRRPBj7z8P7JbtZiFgRhFV+72YYEYnZSJmayKSrMzkjPWOTU9O62xOUYddgemYHHUXW4WwKLp0t0+GMCibrlZChYl61cc0c6e3ikpVjKbuk0a5UU5nSAvZ/LYhBtUTWhEDzt34SL7DD9DfcvMr+zLJ+nggG0wtnKwv1JjG7IODsY8w76ing/9W87i5ffkDJXDxzZlm5iMmbJM9Y+pbRY60S/ZuzrK4sC+bJw5jPrH2Gc444IOPXiuiRl/lajyciAfvVs+o3qCGZkHPck6/Iy5mf5x8yH8somOjVvGYp9acDw7YZmxTGnq8UuvdqJHCOivGrIOWgbVnPNwTI9tYGxA3T/hGnH5pWwuxC+fMP/G9M34GbqTLzIP8zyBxiMuquyaW/phXjvPWuYVrGTcp2tO/smKGfO+zGCdtfp5/T0B3rGxsTeXNgYGemZHJ8fGp54KXLe0LTwegE6qgACAP8RRI0l33p8GviAAAKytAXjcqFtsww2m1Bc3GSDKJQ15gfsI40B5DR/I3QaAnOGSp+BAMmChxbPhsfVj6138s6sI+GeJ31mASQAA7gQfCABinAFuoPlAOBcivCyASAsUKa3/tbWaWuqnejdBqM1hrcZr7S0MOvWfLy+8BgDwfceFS/zsno/da2urf/s5hG+AAA+czwr+wiqy+etcSGvLPYAAfJ9wU6ATLdkByYKglFGJEkagAwEA4AM5eCCCHwB0ABkQEKADcMCBBHgAMHcL7ooUYmwhhZ/zExnVadegWqUqTUTPg+iAffY71jDJpONEJdqJdKpU09er6i1DqVpNqjQz2EOUQO+iM9RQoyZQztPlgZahUGYP0qZkBv24RtWMDET77bGvVngAHWsLcAAAAAA=) format('woff2'); -} -@font-face{ - font-family: "dm"; - font-weight: bold; - font-style: normal; - unicode-range: U+F8FF-10FFFF; - src: local('☺'), - url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJ0AAsAAAAAA/QAAAIqAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYILGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzADAC4D7IZjzymg62laLb5im9YqKYd4pp0IH5Q4wfM11t7f3RNPmCSGpIkh4c2yaCYkPItas2reuIZYSNQTM2Ss3ZtaSFrNsPSi0pFIpbFOplQSozQTgtx2D1DYR6JuhAypGLvREA2A7uApPgY1OYgeC8Ayvq0tfgCOy3/OjT9vz2+AA909pwF9/n14lic0TDDgLsCov3cD7HJP9eTYIyKzTeFwCA8+E4xCRLzVnb12vN/reyL9TgUEkfJgglcieJcPbf0Qt29BkrLv8J7zzxfL3bbhyD1wjGfBvFlGfC+BrvfzhBHx1jVXVjwp4PWnrNv+wl6+cBzQyXu6yQ23capTbj9uoPHOagFvx3+WIwca9UT0znIXQESUt2/589nB/3tmaqS0NLrz8cJ7wfRIj9sRh9955/JPstfOQqZk3y3czExOze66PSH71ADLTBIBnGTKJtW1sRzoGIAEAGBYnnxqGre7c9KDHAkko875Ih2j3eoVNMnzyi47CgTI/R/Y5O+BdBB/hz4QRzmYAVQZiQ3kMgAIIPD/xF1r5/lKD3MM+Fw1wOfx8Ya/07h5I9AoAAT0Q/wvR2PC/4+GwJWJmuVBIwSw0URCjAPogY6CoQcVigHUwNDQ4DoL6WWVtc7zaH+wHDXrjYnekaVEkWKlS6TJfCtX11KvsdHs+BudH+X2pDHteZVAhURkrVZjS6jKUF2dVSteBQb3BfvWuNnv6RUrUMSpBo7ENgAAAA==) format('woff2'); -} diff --git a/docs/md_v1/assets/highlight.css b/docs/md_v1/assets/highlight.css deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/md_v1/assets/highlight.min.js b/docs/md_v1/assets/highlight.min.js deleted file mode 100644 index f43ba9aa..00000000 --- a/docs/md_v1/assets/highlight.min.js +++ /dev/null @@ -1,1213 +0,0 @@ -/*! - Highlight.js v11.9.0 (git: f47103d4f1) - (c) 2006-2023 undefined and other contributors - License: BSD-3-Clause - */ - var hljs=function(){"use strict";function e(n){ - return n instanceof Map?n.clear=n.delete=n.set=()=>{ - throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{ - throw Error("set is read-only") - }),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{ - const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a) - })),n}class n{constructor(e){ - void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} - ignoreMatch(){this.isMatchIgnored=!0}}function t(e){ - return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") - }function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n] - ;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope - ;class r{constructor(e,n){ - this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){ - this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{ - if(e.startsWith("language:"))return e.replace("language:","language-") - ;if(e.includes(".")){const t=e.split(".") - ;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ") - }return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)} - closeNode(e){i(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ - this.buffer+=``}}const s=(e={})=>{const n={children:[]} - ;return Object.assign(n,e),n};class o{constructor(){ - this.rootNode=s(),this.stack=[this.rootNode]}get top(){ - return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ - this.top.children.push(e)}openNode(e){const n=s({scope:e}) - ;this.add(n),this.stack.push(n)}closeNode(){ - if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ - for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} - walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){ - return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n), - n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){ - "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ - o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e} - addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ - this.closeNode()}__addSublanguage(e,n){const t=e.root - ;n&&(t.scope="language:"+n),this.add(t)}toHTML(){ - return new r(this,this.options).value()}finalize(){ - return this.closeAllNodes(),!0}}function c(e){ - return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")} - function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")} - function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{ - const n=e[e.length-1] - ;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} - })(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"} - function p(e){return RegExp(e.toString()+"|").exec("").length-1} - const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ - ;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t - ;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break} - i+=a.substring(0,e.index), - a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0], - "("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)} - const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={ - begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", - illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", - contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n, - contains:[]},t);i.contains.push({scope:"doctag", - begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", - end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) - ;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) - ;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i - },M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({ - __proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{ - scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x, - C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number", - begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{ - "on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{ - n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f, - MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0}, - NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, - PHRASAL_WORDS_MODE:{ - begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ - },QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, - end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}, - RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", - SHEBANG:(e={})=>{const n=/^#![ ]*\// - ;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n, - end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)}, - TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E, - UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){ - "."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){ - void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){ - n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", - e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, - void 0===e.relevance&&(e.relevance=0))}function I(e,n){ - Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){ - if(e.match){ - if(e.begin||e.end)throw Error("begin & end are not supported with match") - ;e.begin=e.match,delete e.match}}function B(e,n){ - void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return - ;if(e.starts)throw Error("beforeMatch cannot be used with starts") - ;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n] - })),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={ - relevance:0,contains:[Object.assign(t,{endsParent:!0})] - },e.relevance=0,delete t.beforeMatch - },z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword" - ;function U(e,n,t=F){const a=Object.create(null) - ;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{ - Object.assign(a,U(e[t],n,t))})),a;function i(e,t){ - n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|") - ;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){ - return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{ - console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{ - P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0) - },G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={} - ;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1]) - ;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{ - e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, - delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ - _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope - }),(e=>{if(Array.isArray(e.begin)){ - if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), - G - ;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), - G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{ - if(Array.isArray(e.end)){ - if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), - G - ;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), - G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){ - function n(n,t){ - return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":"")) - }class t{constructor(){ - this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} - addRule(e,n){ - n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]), - this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) - ;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|" - }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex - ;const n=this.matcherRe.exec(e);if(!n)return null - ;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t] - ;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){ - this.rules=[],this.multiRegexes=[], - this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ - if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t - ;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))), - n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){ - return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){ - this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){ - const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex - ;let t=n.exec(e) - ;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{ - const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)} - return t&&(this.regexIndex+=t.position+1, - this.regexIndex===this.count&&this.considerAll()),t}} - if(e.compilerExtensions||(e.compilerExtensions=[]), - e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") - ;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r - ;if(r.isCompiled)return o - ;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))), - r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null - ;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), - l=r.keywords.$pattern, - delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)), - o.keywordPatternRe=n(l,!0), - s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), - r.end&&(o.endRe=n(o.end)), - o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)), - r.illegal&&(o.illegalRe=n(r.illegal)), - r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{ - variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{ - starts:e.starts?a(e.starts):null - }):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o) - })),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i - ;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin" - }))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end" - }),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){ - return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{ - constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}} - const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{ - const a=Object.create(null),i=Object.create(null),r=[];let s=!0 - ;const o="Could not find the language '{}', did you forget to load/include a language module?",c={ - disableAutodetect:!0,name:"Plain text",contains:[]};let p={ - ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, - languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", - cssSelector:"pre code",languages:null,__emitter:l};function _(e){ - return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i="" - ;"object"==typeof n?(a=e, - t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."), - q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), - i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r) - ;const s=r.result?r.result:f(r.language,r.code,t) - ;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){ - const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A) - ;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t="" - ;for(;n;){t+=A.substring(e,n.index) - ;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){ - const[e,a]=r - ;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{ - const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0] - ;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a - ;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{ - if(""===A)return;let e=null;if("string"==typeof x.subLanguage){ - if(!a[x.subLanguage])return void S.addText(A) - ;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top - }else e=E(A,x.subLanguage.length?x.subLanguage:null) - ;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language) - })():c(),A=""}function g(e,n){ - ""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1 - ;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue} - const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}} - function b(e,n){ - return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope), - e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), - A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{ - value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n) - ;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e) - ;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){ - for(;e.endsParent&&e.parent;)e=e.parent;return e}} - if(e.endsWithParent)return m(e.parent,t,a)}function _(e){ - return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){ - const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x - ;x.endScope&&x.endScope._wrap?(d(), - g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(), - u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n), - d(),r.excludeEnd&&(A=n));do{ - x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent - }while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length} - let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0 - ;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){ - if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`) - ;throw n.languageName=e,n.badRule=y.rule,n}return 1} - if(y=r,"begin"===r.type)return(e=>{ - const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]] - ;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t) - ;return a.skip?A+=t:(a.excludeBegin&&(A+=t), - d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r) - ;if("illegal"===r.type&&!i){ - const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"') - ;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e} - if("illegal"===r.type&&""===o)return 1 - ;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches") - ;return A+=o,o.length}const w=v(e) - ;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"') - ;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[] - ;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope) - ;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{ - if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){ - R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T - ;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e) - ;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e, - value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){ - if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t), - illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T, - context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{ - language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x} - ;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{ - const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)} - ;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1))) - ;i.unshift(t);const r=i.sort(((e,n)=>{ - if(e.relevance!==n.relevance)return n.relevance-e.relevance - ;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1 - ;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s - ;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{ - let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"" - ;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1]) - ;return n||(H(o.replace("{}",t[1])), - H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"} - return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return - ;if(x("before:highlightElement",{el:e,language:t - }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) - ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), - console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), - console.warn("The element with unescaped HTML:"), - console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML) - ;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a) - ;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t - ;e.classList.add("hljs"),e.classList.add("language-"+a) - })(e,t,r.language),e.result={language:r.language,re:r.relevance, - relevance:r.relevance},r.secondBest&&(e.secondBest={ - language:r.secondBest.language,relevance:r.secondBest.relevance - }),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){ - "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0 - }function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]} - function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ - i[e.toLowerCase()]=n}))}function k(e){const n=v(e) - ;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{ - e[t]&&e[t](n)}))} - "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ - N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w, - highlightElement:y, - highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"), - q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, - initHighlighting:()=>{ - w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, - initHighlightingOnLoad:()=>{ - w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") - },registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){ - if(K("Language definition for '{}' could not be registered.".replace("{}",e)), - !s)throw n;K(n),i=c} - i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{ - languageName:e})},unregisterLanguage:e=>{delete a[e] - ;for(const n of Object.keys(i))i[n]===e&&delete i[n]}, - listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O, - autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ - e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{ - e["before:highlightBlock"](Object.assign({block:n.el},n)) - }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{ - e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}, - removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{ - s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b, - lookahead:d,either:m,optional:u,anyNumberOfTimes:g} - ;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t - },te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{ - scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{ - scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/}, - FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/}, - ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", - contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ - scope:"number", - begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", - relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} - }),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le) - ;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={ - className:"number",variants:[{ - begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{ - begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ - begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{ - begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{ - begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{ - begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], - relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))} - const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye) - ;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/, - end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ - const t=e[0].length+e.index,a=e.input[t] - ;if("<"===a||","===a)return void n.ignoreMatch();let i - ;">"===a&&(((e,{after:n})=>{const t="",M={ - match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)], - keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]} - ;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{ - PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/, - contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ - label:"use_strict",className:"meta",relevance:10, - begin:/^\s*['"]use (strict|asm)['"]/ - },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{ - className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{ - begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", - keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{ - className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{ - className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{ - className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, - excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/, - relevance:0},{variants:[{begin:"<>",end:""},{ - match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin, - "on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{ - begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{ - beginKeywords:"while if switch catch for"},{ - begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", - returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t, - className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t, - relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, - contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, - className:"variable.constant"},E,k,{match:/\$[(.]/}]}} - const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] - ;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={ - begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]} - ;Object.assign(t,{className:"variable",variants:[{ - begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={ - className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={ - begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, - end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/, - contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/, - end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] - },l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 - }),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, - contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ - name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, - keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], - literal:["true","false"], - built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] - },contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{ - match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}, - grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}] - }),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ - className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ - match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{ - begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ - begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", - end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ - begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ - className:"number",variants:[{begin:"\\b(0b[01']+)"},{ - begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" - },{ - begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" - }],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ - keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" - },contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ - className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ - className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 - },g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ - keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], - type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], - literal:"true false NULL", - built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" - },b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{ - begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], - keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u, - contains:b.concat(["self"]),relevance:0}]),relevance:0},p={ - begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, - keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ - begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], - relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, - keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/, - end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s] - }]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, - disableAutodetect:!0,illegal:"=]/,contains:[{ - beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, - strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{ - contains:[{begin:/\\\n/}] - }),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ - className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{ - begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ - begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", - end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ - begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ - className:"number",variants:[{begin:"\\b(0b[01']+)"},{ - begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" - },{ - begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" - }],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ - keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" - },contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ - className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ - className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 - },g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ - type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], - keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], - literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], - _type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] - },b={className:"function.dispatch",relevance:0,keywords:{ - _hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] - }, - begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/)) - },m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{ - begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], - keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, - contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function", - begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, - keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ - begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{ - begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{ - className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0, - contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u, - relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}] - },s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++", - aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{ - match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], - className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={ - keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), - built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], - literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{ - begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{ - begin:"\\b(0b[01']+)"},{ - begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ - begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" - }],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] - },r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/, - keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/, - end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ - },e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{ - begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/, - contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]}) - ;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE], - o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{ - illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] - },u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t] - },b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={ - begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], - keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, - contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ - begin:"\x3c!--|--\x3e"},{begin:""}]}] - }),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", - end:"$",keywords:{ - keyword:"if else elif endif define undef warning error line region endregion pragma checksum" - }},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, - illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" - },t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", - relevance:0,end:/[{;=]/,illegal:/[^\s:]/, - contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ - beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, - contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", - begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ - className:"string",begin:/"/,end:/"/}]},{ - beginKeywords:"new return throw await else",relevance:0},{className:"function", - begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, - end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ - beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", - relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, - contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params", - begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, - contains:[g,a,e.C_BLOCK_COMMENT_MODE] - },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{ - const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{ - name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{ - keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"}, - contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ - },t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 - },{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 - },t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ - begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}] - },t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{ - begin:/:/,end:/[;}{]/, - contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{ - begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" - },contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0, - excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]", - relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ - },{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ - $pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{ - begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{ - className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{ - const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{ - className:"meta",relevance:10, - match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) - },{className:"comment",variants:[{ - begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), - end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ - className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, - end:/$/}]}},grmr_go:e=>{const n={ - keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], - type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], - literal:["true","false","iota","nil"], - built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] - };return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex;return{name:"GraphQL",aliases:["gql"], - case_insensitive:!0,disableAutodetect:!1,keywords:{ - keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], - literal:["true","false","null"]}, - contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ - scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", - begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, - end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ - scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)), - relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={ - className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{ - begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/, - end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{ - begin:/\$\{(.*?)\}/}]},r={className:"literal", - begin:/\bon|off|true|false|yes|no\b/},s={className:"string", - contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{ - begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}] - },o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0 - },l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ - name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, - contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{ - begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)), - className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{ - const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={ - keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], - literal:["false","true","null"], - type:["char","boolean","long","float","int","byte","short","double"], - built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{ - begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/, - end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} - ;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/, - contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, - relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ - begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 - },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, - className:"string",contains:[e.BACKSLASH_ESCAPE] - },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ - match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ - 1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ - begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type", - 3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", - 3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ - beginKeywords:"new throw return else",relevance:0},{ - begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ - 2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/, - end:/\)/,keywords:i,relevance:0, - contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE] - },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe, - grmr_json:e=>{const n=["true","false","null"],t={scope:"literal", - beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{ - className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ - match:/[{}[\],:]/,className:"punctuation",relevance:0 - },e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], - illegal:"\\S"}},grmr_kotlin:e=>{const n={ - keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", - built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", - literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" - },a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={ - className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", - variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'", - illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, - contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={ - className:"meta", - begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" - },o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, - end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] - },l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={ - variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, - contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g], - {name:"Kotlin",aliases:["kt","kts"],keywords:n, - contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", - begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword", - begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", - begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$", - returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ - begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, - contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, - keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, - endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, - endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0 - },e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{ - begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ - 3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, - illegal:"extends implements",contains:[{ - beginKeywords:"public protected internal private constructor" - },e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, - excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, - excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env", - end:"$",illegal:"\n"},l]}},grmr_less:e=>{ - const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({ - className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n, - relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only", - attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c, - relevance:0} - ;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{ - begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", - excludeEnd:!0} - },n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{ - className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 - },n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({ - begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0, - contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:", - returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ - },n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b", - end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}] - },m={className:"keyword", - begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", - starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={ - className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a - }],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{ - begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0, - returnEnd:!0,illegal:"[<='$\"]",relevance:0, - contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{ - begin:"\\b("+re.join("|")+")\\b",className:"selector-tag" - },n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{ - className:"selector-pseudo",begin:":("+oe.join("|")+")"},{ - className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/, - end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={ - begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]} - ;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH), - {name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}}, - grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"] - },i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10 - })];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, - literal:"true false nil", - keyword:"and break do else elseif end for goto if in local not or repeat return then until while", - built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" - },contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)", - contains:[e.inherit(e.TITLE_MODE,{ - begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", - begin:"\\(",endsWithParent:!0,contains:i}].concat(i) - },e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", - begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={ - className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", - contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{ - const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={ - variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{ - begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, - relevance:2},{ - begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), - relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ - begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ - },{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, - returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", - excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", - end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[], - variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] - },i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ - begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[] - }),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r) - ;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o) - })),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ - className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{ - begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", - contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", - end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o, - end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ - begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ - begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", - contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ - begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ - className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ - className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{ - const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n, - keyword:["@interface","@class","@protocol","@implementation"]};return{ - name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"], - keywords:{"variable.language":["this","super"],$pattern:n, - keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], - literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], - built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], - type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] - },illegal:"/,end:/$/,illegal:"\\n" - },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", - begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t, - contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, - relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={ - $pattern:/[\w.]+/, - keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" - },i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/, - end:/\}/},s={variants:[{begin:/\$\d/},{ - begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") - },{begin:/[$%@][^\s\w{]/,relevance:0}] - },o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{ - const r="\\1"===i?i:n.concat(i,a) - ;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t) - },d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ - endsWithParent:!0}),r,{className:"string",contains:o,variants:[{ - begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", - end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ - begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", - relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", - contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", - contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ - begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number", - begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", - relevance:0},{ - begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", - keywords:"split return print reverse grep",relevance:0, - contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ - begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{ - begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{ - className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ - begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0 - }),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{ - begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub", - end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{ - begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$", - subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}] - }];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a, - contains:g}},grmr_php:e=>{ - const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={ - scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{ - begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null - }),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ - illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{ - begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, - contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{ - n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{ - n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({ - begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ - begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ - begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ - begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" - }],relevance:0 - },g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={ - keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{ - n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase()) - })),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{ - match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{ - 1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{ - match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" - }},{match:[/::/,/class/],scope:{2:"variable.language"}},{ - match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", - 3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))], - scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class", - 3:"variable.language"}}]},E={scope:"attr", - match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0, - begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_] - },N={relevance:0, - match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)], - scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N) - ;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1, - keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/, - endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{ - begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]}, - contains:["self",...w]},...w,{scope:"meta",match:i}] - },e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ - scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, - keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, - contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ - begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ - begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{ - match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{ - scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, - excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" - },e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", - begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m, - contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{ - beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", - illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ - beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ - beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, - contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ - beginKeywords:"use",relevance:0,end:";",contains:[{ - match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]} - },grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{ - begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", - end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0 - },e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null, - skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null, - contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text", - aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{ - const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ - $pattern:/[A-Za-z]\w+|__\w+__/,keyword:a, - built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], - literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], - type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] - },r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/, - end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={ - className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ - begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, - contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ - begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, - contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ - begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, - contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, - end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/, - relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ - begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, - end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, - contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, - contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] - },c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={ - className:"number",relevance:0,variants:[{ - begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{ - begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{ - begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})` - },{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})` - }]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, - contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ - className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, - end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, - contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{ - name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, - illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if", - relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{ - 1:"keyword",3:"title.function"},contains:[m]},{variants:[{ - match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}], - scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ - className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}}, - grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt", - starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{ - begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{ - const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) - ;return{name:"R",keywords:{$pattern:t, - keyword:"function if in break next repeat else for while", - literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", - built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" - },contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, - starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), - endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ - scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 - }]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] - }),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], - variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ - }),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ - }),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ - }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ - }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ - }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', - relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ - 1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"}, - match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{ - 2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"}, - match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{ - match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`", - contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{ - const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={ - "variable.constant":["__FILE__","__LINE__","__ENCODING__"], - "variable.language":["self","super"], - keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], - built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], - literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={ - begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s] - }),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10 - }),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/, - end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c], - variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ - begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ - begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, - end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ - begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ - begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ - begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ - begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ - begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), - contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, - contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number", - relevance:0,variants:[{ - begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{ - begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" - },{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ - begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ - begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{ - className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, - keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ - match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", - 4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{ - 2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{ - 1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, - className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{ - match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{ - begin:e.IDENT_RE+"::"},{className:"symbol", - begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", - begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable", - begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ - className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, - relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", - keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c], - illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ - begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", - end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l) - ;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m} - },{className:"meta.prompt", - begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", - starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby", - aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/, - contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}}, - grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0, - begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/)) - },a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] - ;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r, - keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], - literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}}, - grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable", - begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", - case_insensitive:!0,illegal:"[=/|']", - contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{ - className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ - className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 - },n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", - begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo", - begin:":("+a.join("|")+")"},{className:"selector-pseudo", - begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/, - contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute", - begin:"\\b("+ce.join("|")+")\\b"},{ - begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" - },{begin:/:/,end:/[;}{]/,relevance:0, - contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH] - },{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{ - begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, - keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i, - className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" - },r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE] - },n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session", - aliases:["console","shellsession"],contains:[{className:"meta.prompt", - begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, - subLanguage:"bash"}}]}),grmr_sql:e=>{ - const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={ - begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} - ;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ - $pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t - ;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)) - })(l,{when:e=>e.length<3}),literal:a,type:i, - built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] - },contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, - keyword:l.concat(s),literal:a,type:i}},{className:"type", - begin:n.either("double precision","large object","with timezone","without timezone") - },c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", - variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, - contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ - className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, - relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0 - },t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={ - match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)), - relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{ - className:"keyword", - match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={ - $pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{ - match:b(/\./,m(...De)),relevance:0},{className:"built_in", - match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{ - className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}] - }],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0, - variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{ - match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/ - },{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{ - match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] - }),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) - }),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/ - }),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)] - }),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={ - className:"string", - variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")] - },k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, - contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, - contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t, - contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={ - scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/) - },C=[A,{className:"variable",match:/\$\d+/},{className:"variable", - match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ - contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{ - scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={ - match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type", - match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+") - },{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{ - match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={ - begin://,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D) - ;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{ - match:b(Fe,/\s*:/),keywords:"_|0",relevance:0 - },...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin://, - keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l, - contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/, - relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params", - match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={ - match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword", - 3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={ - match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, - contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{ - 1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{ - 1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/} - ;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label)) - ;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/, - end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l, - contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor", - end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{ - className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c] - },F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0 - },S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{ - const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={ - beginKeywords:"namespace",end:/\{/,excludeEnd:!0, - contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/, - excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a}, - contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e, - keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), - literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta", - begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n)) - ;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)} - ;return Object.assign(n.keywords,s), - n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]), - l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10, - begin:/^\s*['"]use strict['"]/ - }),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{ - name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{ - const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={ - className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ - begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ - begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}] - },o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] - }),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) - ;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, - classNameAliases:{label:"symbol"},keywords:{ - keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", - built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", - type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", - literal:"true false nothing"}, - illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ - className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, - end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0, - variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ - },{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ - begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ - className:"label",begin:/^\w+:/},o,l,{className:"meta", - begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, - end:/$/,keywords:{ - keyword:"const disable else elseif enable end externalsource if region then"}, - contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/) - ;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, - keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] - },contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/], - className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ - match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ - begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", - 3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, - className:"type"},{className:"keyword", - match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ - },{className:"number",relevance:0, - match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ - }]}},grmr_xml:e=>{ - const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={ - className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/, - contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] - },r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{ - className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={ - endsWithParent:!0,illegal:/`]+/}]}]}]};return{ - name:"HTML, XML", - aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], - case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{ - className:"meta",begin://,contains:[i,r,o,s]}]}] - },e.COMMENT(//,{relevance:10}),{begin://, - relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, - relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", - begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{ - end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", - begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{ - end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ - className:"tag",begin:/<>|<\/>/},{className:"tag", - begin:n.concat(//,/>/,/\s/)))), - end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{ - className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{ - className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} - },grmr_yaml:e=>{ - const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={ - className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ - },{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", - variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{ - variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={ - end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/, - end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]", - contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{ - begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ - begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", - relevance:10},{className:"string", - begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ - begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, - relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type", - begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t - },{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", - begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", - relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ - className:"number", - begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" - },{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l] - ;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0, - aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){ - const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])} - return He}() - ;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); \ No newline at end of file diff --git a/docs/md_v1/assets/highlight_init.js b/docs/md_v1/assets/highlight_init.js deleted file mode 100644 index e2379278..00000000 --- a/docs/md_v1/assets/highlight_init.js +++ /dev/null @@ -1,6 +0,0 @@ -document.addEventListener('DOMContentLoaded', (event) => { - document.querySelectorAll('pre code').forEach((block) => { - hljs.highlightBlock(block); - }); - }); - \ No newline at end of file diff --git a/docs/md_v1/assets/styles.css b/docs/md_v1/assets/styles.css deleted file mode 100644 index f103474f..00000000 --- a/docs/md_v1/assets/styles.css +++ /dev/null @@ -1,153 +0,0 @@ -@font-face { - font-family: "Monaco"; - font-style: normal; - font-weight: normal; - src: local("Monaco"), url("Monaco.woff") format("woff"); -} - -:root { - --global-font-size: 16px; - --global-line-height: 1.5em; - --global-space: 10px; - --font-stack: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, - Courier New, monospace, serif; - --font-stack: dm, Monaco, Courier New, monospace, serif; - --mono-font-stack: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, - Courier New, monospace, serif; - - --background-color: #151515; /* Dark background */ - --font-color: #eaeaea; /* Light font color for contrast */ - --invert-font-color: #151515; /* Dark color for inverted elements */ - --primary-color: #1a95e0; /* Primary color can remain the same or be adjusted for better contrast */ - --secondary-color: #727578; /* Secondary color for less important text */ - --error-color: #ff5555; /* Bright color for errors */ - --progress-bar-background: #444; /* Darker background for progress bar */ - --progress-bar-fill: #1a95e0; /* Bright color for progress bar fill */ - --code-bg-color: #1e1e1e; /* Darker background for code blocks */ - --input-style: solid; /* Keeping input style solid */ - --block-background-color: #202020; /* Darker background for block elements */ - --global-font-color: #eaeaea; /* Light font color for global elements */ - - --background-color: #222225; - - --background-color: #070708; - --page-width: 70em; - --font-color: #e8e9ed; - --invert-font-color: #222225; - --secondary-color: #a3abba; - --secondary-color: #d5cec0; - --tertiary-color: #a3abba; - --primary-color: #09b5a5; /* Updated to the brand color */ - --primary-color: #50ffff; /* Updated to the brand color */ - --error-color: #ff3c74; - --progress-bar-background: #3f3f44; - --progress-bar-fill: #09b5a5; /* Updated to the brand color */ - --code-bg-color: #3f3f44; - --input-style: solid; - --display-h1-decoration: none; - - --display-h1-decoration: none; -} - -/* body { - background-color: var(--background-color); - color: var(--font-color); -} - -a { - color: var(--primary-color); -} - -a:hover { - background-color: var(--primary-color); - color: var(--invert-font-color); -} - -blockquote::after { - color: #444; -} - -pre, code { - background-color: var(--code-bg-color); - color: var(--font-color); -} - -.terminal-nav:first-child { - border-bottom: 1px dashed var(--secondary-color); -} */ - -.terminal-mkdocs-main-content { - line-height: var(--global-line-height); -} - -strong, -.highlight { - /* background: url(//s2.svgbox.net/pen-brushes.svg?ic=brush-1&color=50ffff); */ - background-color: #50ffff33; -} - -.terminal-card > header { - color: var(--font-color); - text-align: center; - background-color: var(--progress-bar-background); - padding: 0.3em 0.5em; -} -.btn.btn-sm { - color: var(--font-color); - padding: 0.2em 0.5em; - font-size: 0.8em; -} - -.loading-message { - display: none; - margin-top: 20px; -} - -.response-section { - display: none; - padding-top: 20px; -} - -.tabs { - display: flex; - flex-direction: column; -} -.tab-list { - display: flex; - padding: 0; - margin: 0; - list-style-type: none; - border-bottom: 1px solid var(--font-color); -} -.tab-item { - cursor: pointer; - padding: 10px; - border: 1px solid var(--font-color); - margin-right: -1px; - border-bottom: none; -} -.tab-item:hover, -.tab-item:focus, -.tab-item:active { - background-color: var(--progress-bar-background); -} -.tab-content { - display: none; - border: 1px solid var(--font-color); - border-top: none; -} -.tab-content:first-of-type { - display: block; -} - -.tab-content header { - padding: 0.5em; - display: flex; - justify-content: end; - align-items: center; - background-color: var(--progress-bar-background); -} -.tab-content pre { - margin: 0; - max-height: 300px; overflow: auto; border:none; -} \ No newline at end of file diff --git a/docs/md_v1/changelog.md b/docs/md_v1/changelog.md deleted file mode 100644 index 03e490f6..00000000 --- a/docs/md_v1/changelog.md +++ /dev/null @@ -1,102 +0,0 @@ -# Changelog - -## [v0.2.77] - 2024-08-04 - -Significant improvements in text processing and performance: - -- 🚀 **Dependency reduction**: Removed dependency on spaCy model for text chunk labeling in cosine extraction strategy. -- 🤖 **Transformer upgrade**: Implemented text sequence classification using a transformer model for labeling text chunks. -- ⚡ **Performance enhancement**: Improved model loading speed due to removal of spaCy dependency. -- 🔧 **Future-proofing**: Laid groundwork for potential complete removal of spaCy dependency in future versions. - -These changes address issue #68 and provide a foundation for faster, more efficient text processing in Crawl4AI. - -## [v0.2.76] - 2024-08-02 - -Major improvements in functionality, performance, and cross-platform compatibility! 🚀 - -- 🐳 **Docker enhancements**: Significantly improved Dockerfile for easy installation on Linux, Mac, and Windows. -- 🌐 **Official Docker Hub image**: Launched our first official image on Docker Hub for streamlined deployment. -- 🔧 **Selenium upgrade**: Removed dependency on ChromeDriver, now using Selenium's built-in capabilities for better compatibility. -- 🖼️ **Image description**: Implemented ability to generate textual descriptions for extracted images from web pages. -- ⚡ **Performance boost**: Various improvements to enhance overall speed and performance. - -A big shoutout to our amazing community contributors: -- [@aravindkarnam](https://github.com/aravindkarnam) for developing the textual description extraction feature. -- [@FractalMind](https://github.com/FractalMind) for creating the first official Docker Hub image and fixing Dockerfile errors. -- [@ketonkss4](https://github.com/ketonkss4) for identifying Selenium's new capabilities, helping us reduce dependencies. - -Your contributions are driving Crawl4AI forward! 🙌 - -## [v0.2.75] - 2024-07-19 - -Minor improvements for a more maintainable codebase: - -- 🔄 Fixed typos in `chunking_strategy.py` and `crawler_strategy.py` to improve code readability -- 🔄 Removed `.test_pads/` directory from `.gitignore` to keep our repository clean and organized - -These changes may seem small, but they contribute to a more stable and sustainable codebase. By fixing typos and updating our `.gitignore` settings, we're ensuring that our code is easier to maintain and scale in the long run. - - -## v0.2.74 - 2024-07-08 -A slew of exciting updates to improve the crawler's stability and robustness! 🎉 - -- 💻 **UTF encoding fix**: Resolved the Windows \"charmap\" error by adding UTF encoding. -- 🛡️ **Error handling**: Implemented MaxRetryError exception handling in LocalSeleniumCrawlerStrategy. -- 🧹 **Input sanitization**: Improved input sanitization and handled encoding issues in LLMExtractionStrategy. -- 🚮 **Database cleanup**: Removed existing database file and initialized a new one. - -## [v0.2.73] - 2024-07-03 - -💡 In this release, we've bumped the version to v0.2.73 and refreshed our documentation to ensure you have the best experience with our project. - -* Supporting website need "with-head" mode to crawl the website with head. -* Fixing the installation issues for setup.py and dockerfile. -* Resolve multiple issues. - -## [v0.2.72] - 2024-06-30 - -This release brings exciting updates and improvements to our project! 🎉 - -* 📚 **Documentation Updates**: Our documentation has been revamped to reflect the latest changes and additions. -* 🚀 **New Modes in setup.py**: We've added support for three new modes in setup.py: default, torch, and transformers. This enhances the project's flexibility and usability. -* 🐳 **Docker File Updates**: The Docker file has been updated to ensure seamless compatibility with the new modes and improvements. -* 🕷️ **Temporary Solution for Headless Crawling**: We've implemented a temporary solution to overcome issues with crawling websites in headless mode. - -These changes aim to improve the overall user experience, provide more flexibility, and enhance the project's performance. We're thrilled to share these updates with you and look forward to continuing to evolve and improve our project! - -## [0.2.71] - 2024-06-26 - -**Improved Error Handling and Performance** 🚧 - -* 🚫 Refactored `crawler_strategy.py` to handle exceptions and provide better error messages, making it more robust and reliable. -* 💻 Optimized the `get_content_of_website_optimized` function in `utils.py` for improved performance, reducing potential bottlenecks. -* 💻 Updated `utils.py` with the latest changes, ensuring consistency and accuracy. -* 🚫 Migrated to `ChromeDriverManager` to resolve Chrome driver download issues, providing a smoother user experience. - -These changes focus on refining the existing codebase, resulting in a more stable, efficient, and user-friendly experience. With these improvements, you can expect fewer errors and better performance in the crawler strategy and utility functions. - -## [0.2.71] - 2024-06-25 -### Fixed -- Speed up twice the extraction function. - -## [0.2.6] - 2024-06-22 -### Fixed -- Fix issue #19: Update Dockerfile to ensure compatibility across multiple platforms. - -## [0.2.5] - 2024-06-18 -### Added -- Added five important hooks to the crawler: - - on_driver_created: Called when the driver is ready for initializations. - - before_get_url: Called right before Selenium fetches the URL. - - after_get_url: Called after Selenium fetches the URL. - - before_return_html: Called when the data is parsed and ready. - - on_user_agent_updated: Called when the user changes the user_agent, causing the driver to reinitialize. -- Added an example in `quickstart.py` in the example folder under the docs. -- Enhancement issue #24: Replaced inline HTML tags (e.g., DEL, INS, SUB, ABBR) with textual format for better context handling in LLM. -- Maintaining the semantic context of inline tags (e.g., abbreviation, DEL, INS) for improved LLM-friendliness. -- Updated Dockerfile to ensure compatibility across multiple platforms (Hopefully!). - -## [0.2.4] - 2024-06-17 -### Fixed -- Fix issue #22: Use MD5 hash for caching HTML files to handle long URLs diff --git a/docs/md_v1/contact.md b/docs/md_v1/contact.md deleted file mode 100644 index 85faa217..00000000 --- a/docs/md_v1/contact.md +++ /dev/null @@ -1,25 +0,0 @@ -# Contact -If you have any questions, suggestions, or feedback, please feel free to reach out to us: - -- GitHub: [unclecode](https://github.com/unclecode) -- Twitter: [@unclecode](https://twitter.com/unclecode) -- Website: [crawl4ai.com](https://crawl4ai.com) - - -## Contributing 🤝 - -We welcome contributions from the open-source community to help improve Crawl4AI and make it even more valuable for AI enthusiasts and developers. To contribute, please follow these steps: - -1. Fork the repository. -2. Create a new branch for your feature or bug fix. -3. Make your changes and commit them with descriptive messages. -4. Push your changes to your forked repository. -5. Submit a pull request to the main repository. - -For more information on contributing, please see our [contribution guidelines](https://github.com/unclecode/crawl4ai/blob/main/CONTRIBUTING.md). - -## License 📄 - -Crawl4AI is released under the [Apache 2.0 License](https://github.com/unclecode/crawl4ai/blob/main/LICENSE). - -Let's work together to make the web more accessible and useful for AI applications! 💪🌐🤖 diff --git a/docs/md_v1/demo.md b/docs/md_v1/demo.md deleted file mode 100644 index 4ca6c754..00000000 --- a/docs/md_v1/demo.md +++ /dev/null @@ -1,231 +0,0 @@ -# Interactive Demo for Crowler -

-
-
- Enter URL and Options -
- - -
-
- - -
-
- -
- -
-
- -
-
Loading... Please wait.
-
- -
-

Response

-
-
    -
  • Markdown
  • -
  • Cleaned HTML
  • -
  • Media
  • -
  • Extracted Content
  • -
  • Screenshot
  • -
  • Python Code
  • -
-
-
-
- - -
-
-
-
- - - - - - - - - - -
-
- - - - -
\ No newline at end of file diff --git a/docs/md_v1/examples/hooks_auth.md b/docs/md_v1/examples/hooks_auth.md deleted file mode 100644 index e4b7d7ce..00000000 --- a/docs/md_v1/examples/hooks_auth.md +++ /dev/null @@ -1,110 +0,0 @@ -# Hooks & Auth for AsyncWebCrawler - -Crawl4AI's AsyncWebCrawler allows you to customize the behavior of the web crawler using hooks. Hooks are asynchronous functions that are called at specific points in the crawling process, allowing you to modify the crawler's behavior or perform additional actions. This example demonstrates how to use various hooks to customize the asynchronous crawling process. - -## Example: Using Crawler Hooks with AsyncWebCrawler - -Let's see how we can customize the AsyncWebCrawler using hooks! In this example, we'll: - -1. Configure the browser when it's created. -2. Add custom headers before navigating to the URL. -3. Log the current URL after navigation. -4. Perform actions after JavaScript execution. -5. Log the length of the HTML before returning it. - -### Hook Definitions - -```python -import asyncio -from crawl4ai import AsyncWebCrawler -from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy -from playwright.async_api import Page, Browser - -async def on_browser_created(browser: Browser): - print("[HOOK] on_browser_created") - # Example customization: set browser viewport size - context = await browser.new_context(viewport={'width': 1920, 'height': 1080}) - page = await context.new_page() - - # Example customization: logging in to a hypothetical website - await page.goto('https://example.com/login') - await page.fill('input[name="username"]', 'testuser') - await page.fill('input[name="password"]', 'password123') - await page.click('button[type="submit"]') - await page.wait_for_selector('#welcome') - - # Add a custom cookie - await context.add_cookies([{'name': 'test_cookie', 'value': 'cookie_value', 'url': 'https://example.com'}]) - - await page.close() - await context.close() - -async def before_goto(page: Page): - print("[HOOK] before_goto") - # Example customization: add custom headers - await page.set_extra_http_headers({'X-Test-Header': 'test'}) - -async def after_goto(page: Page): - print("[HOOK] after_goto") - # Example customization: log the URL - print(f"Current URL: {page.url}") - -async def on_execution_started(page: Page): - print("[HOOK] on_execution_started") - # Example customization: perform actions after JS execution - await page.evaluate("console.log('Custom JS executed')") - -async def before_return_html(page: Page, html: str): - print("[HOOK] before_return_html") - # Example customization: log the HTML length - print(f"HTML length: {len(html)}") - return page -``` - -### Using the Hooks with the AsyncWebCrawler - -```python -import asyncio -from crawl4ai import AsyncWebCrawler -from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy - -async def main(): - print("\n🔗 Using Crawler Hooks: Let's see how we can customize the AsyncWebCrawler using hooks!") - - crawler_strategy = AsyncPlaywrightCrawlerStrategy(verbose=True) - crawler_strategy.set_hook('on_browser_created', on_browser_created) - crawler_strategy.set_hook('before_goto', before_goto) - crawler_strategy.set_hook('after_goto', after_goto) - crawler_strategy.set_hook('on_execution_started', on_execution_started) - crawler_strategy.set_hook('before_return_html', before_return_html) - - async with AsyncWebCrawler(verbose=True, crawler_strategy=crawler_strategy) as crawler: - result = await crawler.arun( - url="https://example.com", - js_code="window.scrollTo(0, document.body.scrollHeight);", - wait_for="footer" - ) - - print("📦 Crawler Hooks result:") - print(result) - -asyncio.run(main()) -``` - -### Explanation - -- `on_browser_created`: This hook is called when the Playwright browser is created. It sets up the browser context, logs in to a website, and adds a custom cookie. -- `before_goto`: This hook is called right before Playwright navigates to the URL. It adds custom HTTP headers. -- `after_goto`: This hook is called after Playwright navigates to the URL. It logs the current URL. -- `on_execution_started`: This hook is called after any custom JavaScript is executed. It performs additional JavaScript actions. -- `before_return_html`: This hook is called before returning the HTML content. It logs the length of the HTML content. - -### Additional Ideas - -- **Handling authentication**: Use the `on_browser_created` hook to handle login processes or set authentication tokens. -- **Dynamic header modification**: Modify headers based on the target URL or other conditions in the `before_goto` hook. -- **Content verification**: Use the `after_goto` hook to verify that the expected content is present on the page. -- **Custom JavaScript injection**: Inject and execute custom JavaScript using the `on_execution_started` hook. -- **Content preprocessing**: Modify or analyze the HTML content in the `before_return_html` hook before it's returned. - -By using these hooks, you can customize the behavior of the AsyncWebCrawler to suit your specific needs, including handling authentication, modifying requests, and preprocessing content. \ No newline at end of file diff --git a/docs/md_v1/examples/index.md b/docs/md_v1/examples/index.md deleted file mode 100644 index a6634159..00000000 --- a/docs/md_v1/examples/index.md +++ /dev/null @@ -1,33 +0,0 @@ -# Examples - -Welcome to the examples section of Crawl4AI documentation! In this section, you will find practical examples demonstrating how to use Crawl4AI for various web crawling and data extraction tasks. Each example is designed to showcase different features and capabilities of the library. - -## Examples Index - -### [LLM Extraction](llm_extraction.md) - -This example demonstrates how to use Crawl4AI to extract information using Large Language Models (LLMs). You will learn how to configure the `LLMExtractionStrategy` to get structured data from web pages. - -### [JSON CSS Extraction](json_css_extraction.md) - -This example demonstrates how to use Crawl4AI to extract structured data without using LLM, and just focusing on page structure. You will learn how to use the `JsonCssExtractionStrategy` to extract data using CSS selectors. - -### [JS Execution & CSS Filtering](js_execution_css_filtering.md) - -Learn how to execute custom JavaScript code and filter data using CSS selectors. This example shows how to perform complex web interactions and extract specific content from web pages. - -### [Hooks & Auth](hooks_auth.md) - -This example covers the use of custom hooks for authentication and other pre-crawling tasks. You will see how to set up hooks to modify headers, authenticate sessions, and perform other preparatory actions before crawling. - -### [Summarization](summarization.md) - -Discover how to use Crawl4AI to summarize web page content. This example demonstrates the summarization capabilities of the library, helping you extract concise information from lengthy web pages. - -### [Research Assistant](research_assistant.md) - -In this example, Crawl4AI is used as a research assistant to gather and organize information from multiple sources. You will learn how to use various extraction and chunking strategies to compile a comprehensive report. - ---- - -Each example includes detailed explanations and code snippets to help you understand and implement the features in your projects. Click on the links to explore each example and start making the most of Crawl4AI! diff --git a/docs/md_v1/examples/js_execution_css_filtering.md b/docs/md_v1/examples/js_execution_css_filtering.md deleted file mode 100644 index b45a6602..00000000 --- a/docs/md_v1/examples/js_execution_css_filtering.md +++ /dev/null @@ -1,104 +0,0 @@ -# JS Execution & CSS Filtering with AsyncWebCrawler - -In this example, we'll demonstrate how to use Crawl4AI's AsyncWebCrawler to execute JavaScript, filter data with CSS selectors, and use a cosine similarity strategy to extract relevant content. This approach is particularly useful when you need to interact with dynamic content on web pages, such as clicking "Load More" buttons. - -## Example: Extracting Structured Data Asynchronously - -```python -import asyncio -from crawl4ai import AsyncWebCrawler -from crawl4ai.chunking_strategy import RegexChunking -from crawl4ai.extraction_strategy import CosineStrategy -from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy - -async def main(): - # Define the JavaScript code to click the "Load More" button - js_code = """ - const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); - if (loadMoreButton) { - loadMoreButton.click(); - // Wait for new content to load - await new Promise(resolve => setTimeout(resolve, 2000)); - } - """ - - # Define a wait_for function to ensure content is loaded - wait_for = """ - () => { - const articles = document.querySelectorAll('article.tease-card'); - return articles.length > 10; - } - """ - - async with AsyncWebCrawler(verbose=True) as crawler: - # Run the crawler with keyword filtering and CSS selector - result = await crawler.arun( - url="https://www.nbcnews.com/business", - js_code=js_code, - wait_for=wait_for, - css_selector="article.tease-card", - extraction_strategy=CosineStrategy( - semantic_filter="technology", - ), - chunking_strategy=RegexChunking(), - ) - - # Display the extracted result - print(result.extracted_content) - -# Run the async function -asyncio.run(main()) -``` - -### Explanation - -1. **Asynchronous Execution**: We use `AsyncWebCrawler` with async/await syntax for non-blocking execution. - -2. **JavaScript Execution**: The `js_code` variable contains JavaScript code that simulates clicking a "Load More" button and waits for new content to load. - -3. **Wait Condition**: The `wait_for` function ensures that the page has loaded more than 10 articles before proceeding with the extraction. - -4. **CSS Selector**: The `css_selector="article.tease-card"` parameter ensures that only article cards are extracted from the web page. - -5. **Extraction Strategy**: The `CosineStrategy` is used with a semantic filter for "technology" to extract relevant content based on cosine similarity. - -6. **Chunking Strategy**: We use `RegexChunking()` to split the content into manageable chunks for processing. - -## Advanced Usage: Custom Session and Multiple Requests - -For more complex scenarios where you need to maintain state across multiple requests or execute additional JavaScript after the initial page load, you can use a custom session: - -```python -async def advanced_crawl(): - async with AsyncWebCrawler(verbose=True) as crawler: - # Initial crawl with custom session - result1 = await crawler.arun( - url="https://www.nbcnews.com/business", - js_code=js_code, - wait_for=wait_for, - css_selector="article.tease-card", - session_id="business_session" - ) - - # Execute additional JavaScript in the same session - result2 = await crawler.crawler_strategy.execute_js( - session_id="business_session", - js_code="window.scrollTo(0, document.body.scrollHeight);", - wait_for_js="() => window.innerHeight + window.scrollY >= document.body.offsetHeight" - ) - - # Process results - print("Initial crawl result:", result1.extracted_content) - print("Additional JS execution result:", result2.html) - -asyncio.run(advanced_crawl()) -``` - -This advanced example demonstrates how to: -1. Use a custom session to maintain state across requests. -2. Execute additional JavaScript after the initial page load. -3. Wait for specific conditions using JavaScript functions. - -## Try It Yourself - -These examples demonstrate the power and flexibility of Crawl4AI's AsyncWebCrawler in handling complex web interactions and extracting meaningful data asynchronously. You can customize the JavaScript code, CSS selectors, extraction strategies, and waiting conditions to suit your specific requirements. \ No newline at end of file diff --git a/docs/md_v1/examples/json_css_extraction.md b/docs/md_v1/examples/json_css_extraction.md deleted file mode 100644 index 933d1f68..00000000 --- a/docs/md_v1/examples/json_css_extraction.md +++ /dev/null @@ -1,142 +0,0 @@ -# JSON CSS Extraction Strategy with AsyncWebCrawler - -The `JsonCssExtractionStrategy` is a powerful feature of Crawl4AI that allows you to extract structured data from web pages using CSS selectors. This method is particularly useful when you need to extract specific data points from a consistent HTML structure, such as tables or repeated elements. Here's how to use it with the AsyncWebCrawler. - -## Overview - -The `JsonCssExtractionStrategy` works by defining a schema that specifies: -1. A base CSS selector for the repeating elements -2. Fields to extract from each element, each with its own CSS selector - -This strategy is fast and efficient, as it doesn't rely on external services like LLMs for extraction. - -## Example: Extracting Cryptocurrency Prices from Coinbase - -Let's look at an example that extracts cryptocurrency prices from the Coinbase explore page. - -```python -import json -import asyncio -from crawl4ai import AsyncWebCrawler -from crawl4ai.extraction_strategy import JsonCssExtractionStrategy - -async def extract_structured_data_using_css_extractor(): - print("\n--- Using JsonCssExtractionStrategy for Fast Structured Output ---") - - # Define the extraction schema - schema = { - "name": "Coinbase Crypto Prices", - "baseSelector": ".cds-tableRow-t45thuk", - "fields": [ - { - "name": "crypto", - "selector": "td:nth-child(1) h2", - "type": "text", - }, - { - "name": "symbol", - "selector": "td:nth-child(1) p", - "type": "text", - }, - { - "name": "price", - "selector": "td:nth-child(2)", - "type": "text", - } - ], - } - - # Create the extraction strategy - extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) - - # Use the AsyncWebCrawler with the extraction strategy - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://www.coinbase.com/explore", - extraction_strategy=extraction_strategy, - bypass_cache=True, - ) - - assert result.success, "Failed to crawl the page" - - # Parse the extracted content - crypto_prices = json.loads(result.extracted_content) - print(f"Successfully extracted {len(crypto_prices)} cryptocurrency prices") - print(json.dumps(crypto_prices[0], indent=2)) - - return crypto_prices - -# Run the async function -asyncio.run(extract_structured_data_using_css_extractor()) -``` - -## Explanation of the Schema - -The schema defines how to extract the data: - -- `name`: A descriptive name for the extraction task. -- `baseSelector`: The CSS selector for the repeating elements (in this case, table rows). -- `fields`: An array of fields to extract from each element: - - `name`: The name to give the extracted data. - - `selector`: The CSS selector to find the specific data within the base element. - - `type`: The type of data to extract (usually "text" for textual content). - -## Advantages of JsonCssExtractionStrategy - -1. **Speed**: CSS selectors are fast to execute, making this method efficient for large datasets. -2. **Precision**: You can target exactly the elements you need. -3. **Structured Output**: The result is already structured as JSON, ready for further processing. -4. **No External Dependencies**: Unlike LLM-based strategies, this doesn't require any API calls to external services. - -## Tips for Using JsonCssExtractionStrategy - -1. **Inspect the Page**: Use browser developer tools to identify the correct CSS selectors. -2. **Test Selectors**: Verify your selectors in the browser console before using them in the script. -3. **Handle Dynamic Content**: If the page uses JavaScript to load content, you may need to combine this with JS execution (see the Advanced Usage section). -4. **Error Handling**: Always check the `result.success` flag and handle potential failures. - -## Advanced Usage: Combining with JavaScript Execution - -For pages that load data dynamically, you can combine the `JsonCssExtractionStrategy` with JavaScript execution: - -```python -async def extract_dynamic_structured_data(): - schema = { - "name": "Dynamic Crypto Prices", - "baseSelector": ".crypto-row", - "fields": [ - {"name": "name", "selector": ".crypto-name", "type": "text"}, - {"name": "price", "selector": ".crypto-price", "type": "text"}, - ] - } - - js_code = """ - window.scrollTo(0, document.body.scrollHeight); - await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for 2 seconds - """ - - extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) - - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://example.com/crypto-prices", - extraction_strategy=extraction_strategy, - js_code=js_code, - wait_for=".crypto-row:nth-child(20)", # Wait for 20 rows to load - bypass_cache=True, - ) - - crypto_data = json.loads(result.extracted_content) - print(f"Extracted {len(crypto_data)} cryptocurrency entries") - -asyncio.run(extract_dynamic_structured_data()) -``` - -This advanced example demonstrates how to: -1. Execute JavaScript to trigger dynamic content loading. -2. Wait for a specific condition (20 rows loaded) before extraction. -3. Extract data from the dynamically loaded content. - -By mastering the `JsonCssExtractionStrategy`, you can efficiently extract structured data from a wide variety of web pages, making it a valuable tool in your web scraping toolkit. - -For more details on schema definitions and advanced extraction strategies, check out the[Advanced JsonCssExtraction](../full_details/advanced_jsoncss_extraction.md). \ No newline at end of file diff --git a/docs/md_v1/examples/llm_extraction.md b/docs/md_v1/examples/llm_extraction.md deleted file mode 100644 index f4bd74c8..00000000 --- a/docs/md_v1/examples/llm_extraction.md +++ /dev/null @@ -1,179 +0,0 @@ -# LLM Extraction with AsyncWebCrawler - -Crawl4AI's AsyncWebCrawler allows you to use Language Models (LLMs) to extract structured data or relevant content from web pages asynchronously. Below are two examples demonstrating how to use `LLMExtractionStrategy` for different purposes with the AsyncWebCrawler. - -## Example 1: Extract Structured Data - -In this example, we use the `LLMExtractionStrategy` to extract structured data (model names and their fees) from the OpenAI pricing page. - -```python -import os -import json -import asyncio -from crawl4ai import AsyncWebCrawler -from crawl4ai.extraction_strategy import LLMExtractionStrategy -from pydantic import BaseModel, Field - -class OpenAIModelFee(BaseModel): - model_name: str = Field(..., description="Name of the OpenAI model.") - input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") - output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") - -async def extract_openai_fees(): - url = 'https://openai.com/api/pricing/' - - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url=url, - word_count_threshold=1, - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - schema=OpenAIModelFee.model_json_schema(), - extraction_type="schema", - instruction="From the crawled content, extract all mentioned model names along with their " - "fees for input and output tokens. Make sure not to miss anything in the entire content. " - 'One extracted model JSON format should look like this: ' - '{ "model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens" }' - ), - bypass_cache=True, - ) - - model_fees = json.loads(result.extracted_content) - print(f"Number of models extracted: {len(model_fees)}") - - with open(".data/openai_fees.json", "w", encoding="utf-8") as f: - json.dump(model_fees, f, indent=2) - -asyncio.run(extract_openai_fees()) -``` - -## Example 2: Extract Relevant Content - -In this example, we instruct the LLM to extract only content related to technology from the NBC News business page. - -```python -import os -import json -import asyncio -from crawl4ai import AsyncWebCrawler -from crawl4ai.extraction_strategy import LLMExtractionStrategy - -async def extract_tech_content(): - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://www.nbcnews.com/business", - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - instruction="Extract only content related to technology" - ), - bypass_cache=True, - ) - - tech_content = json.loads(result.extracted_content) - print(f"Number of tech-related items extracted: {len(tech_content)}") - - with open(".data/tech_content.json", "w", encoding="utf-8") as f: - json.dump(tech_content, f, indent=2) - -asyncio.run(extract_tech_content()) -``` - -## Advanced Usage: Combining JS Execution with LLM Extraction - -This example demonstrates how to combine JavaScript execution with LLM extraction to handle dynamic content: - -```python -async def extract_dynamic_content(): - js_code = """ - const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); - if (loadMoreButton) { - loadMoreButton.click(); - await new Promise(resolve => setTimeout(resolve, 2000)); - } - """ - - wait_for = """ - () => { - const articles = document.querySelectorAll('article.tease-card'); - return articles.length > 10; - } - """ - - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://www.nbcnews.com/business", - js_code=js_code, - wait_for=wait_for, - css_selector="article.tease-card", - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - instruction="Summarize each article, focusing on technology-related content" - ), - bypass_cache=True, - ) - - summaries = json.loads(result.extracted_content) - print(f"Number of summarized articles: {len(summaries)}") - - with open(".data/tech_summaries.json", "w", encoding="utf-8") as f: - json.dump(summaries, f, indent=2) - -asyncio.run(extract_dynamic_content()) -``` - -## Customizing LLM Provider - -Crawl4AI uses the `litellm` library under the hood, which allows you to use any LLM provider you want. Just pass the correct model name and API token: - -```python -extraction_strategy=LLMExtractionStrategy( - provider="your_llm_provider/model_name", - api_token="your_api_token", - instruction="Your extraction instruction" -) -``` - -This flexibility allows you to integrate with various LLM providers and tailor the extraction process to your specific needs. - -## Error Handling and Retries - -When working with external LLM APIs, it's important to handle potential errors and implement retry logic. Here's an example of how you might do this: - -```python -import asyncio -from tenacity import retry, stop_after_attempt, wait_exponential - -class LLMExtractionError(Exception): - pass - -@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) -async def extract_with_retry(crawler, url, extraction_strategy): - try: - result = await crawler.arun(url=url, extraction_strategy=extraction_strategy, bypass_cache=True) - return json.loads(result.extracted_content) - except Exception as e: - raise LLMExtractionError(f"Failed to extract content: {str(e)}") - -async def main(): - async with AsyncWebCrawler(verbose=True) as crawler: - try: - content = await extract_with_retry( - crawler, - "https://www.example.com", - LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - instruction="Extract and summarize main points" - ) - ) - print("Extracted content:", content) - except LLMExtractionError as e: - print(f"Extraction failed after retries: {e}") - -asyncio.run(main()) -``` - -This example uses the `tenacity` library to implement a retry mechanism with exponential backoff, which can help handle temporary failures or rate limiting from the LLM API. \ No newline at end of file diff --git a/docs/md_v1/examples/research_assistant.md b/docs/md_v1/examples/research_assistant.md deleted file mode 100644 index 61886f9e..00000000 --- a/docs/md_v1/examples/research_assistant.md +++ /dev/null @@ -1,220 +0,0 @@ -# Research Assistant Example with AsyncWebCrawler - -This example demonstrates how to build an advanced research assistant using `Chainlit`, `Crawl4AI`'s `AsyncWebCrawler`, and various AI services. The assistant can crawl web pages asynchronously, answer questions based on the crawled content, and handle audio inputs. - -## Step-by-Step Guide - -1. **Install Required Packages** - - Ensure you have the necessary packages installed: - - ```bash - pip install chainlit groq openai crawl4ai - ``` - -2. **Import Libraries** - - ```python - import os - import time - import asyncio - from openai import AsyncOpenAI - import chainlit as cl - import re - from io import BytesIO - from chainlit.element import ElementBased - from groq import Groq - from crawl4ai import AsyncWebCrawler - from crawl4ai.extraction_strategy import NoExtractionStrategy - from crawl4ai.chunking_strategy import RegexChunking - - client = AsyncOpenAI(base_url="https://api.groq.com/openai/v1", api_key=os.getenv("GROQ_API_KEY")) - - # Instrument the OpenAI client - cl.instrument_openai() - ``` - -3. **Set Configuration** - - ```python - settings = { - "model": "llama3-8b-8192", - "temperature": 0.5, - "max_tokens": 500, - "top_p": 1, - "frequency_penalty": 0, - "presence_penalty": 0, - } - ``` - -4. **Define Utility Functions** - - ```python - def extract_urls(text): - url_pattern = re.compile(r'(https?://\S+)') - return url_pattern.findall(text) - - async def crawl_urls(urls): - async with AsyncWebCrawler(verbose=True) as crawler: - results = await crawler.arun_many( - urls=urls, - word_count_threshold=10, - extraction_strategy=NoExtractionStrategy(), - chunking_strategy=RegexChunking(), - bypass_cache=True - ) - return [result.markdown for result in results if result.success] - ``` - -5. **Initialize Chat Start Event** - - ```python - @cl.on_chat_start - async def on_chat_start(): - cl.user_session.set("session", { - "history": [], - "context": {} - }) - await cl.Message(content="Welcome to the chat! How can I assist you today?").send() - ``` - -6. **Handle Incoming Messages** - - ```python - @cl.on_message - async def on_message(message: cl.Message): - user_session = cl.user_session.get("session") - - # Extract URLs from the user's message - urls = extract_urls(message.content) - - if urls: - crawled_contents = await crawl_urls(urls) - for url, content in zip(urls, crawled_contents): - ref_number = f"REF_{len(user_session['context']) + 1}" - user_session["context"][ref_number] = { - "url": url, - "content": content - } - - user_session["history"].append({ - "role": "user", - "content": message.content - }) - - # Create a system message that includes the context - context_messages = [ - f'\n{data["content"]}\n' - for ref, data in user_session["context"].items() - ] - system_message = { - "role": "system", - "content": ( - "You are a helpful bot. Use the following context for answering questions. " - "Refer to the sources using the REF number in square brackets, e.g., [1], only if the source is given in the appendices below.\n\n" - "If the question requires any information from the provided appendices or context, refer to the sources. " - "If not, there is no need to add a references section. " - "At the end of your response, provide a reference section listing the URLs and their REF numbers only if sources from the appendices were used.\n\n" - "\n\n".join(context_messages) - ) if context_messages else "You are a helpful assistant." - } - - msg = cl.Message(content="") - await msg.send() - - # Get response from the LLM - stream = await client.chat.completions.create( - messages=[system_message, *user_session["history"]], - stream=True, - **settings - ) - - assistant_response = "" - async for part in stream: - if token := part.choices[0].delta.content: - assistant_response += token - await msg.stream_token(token) - - # Add assistant message to the history - user_session["history"].append({ - "role": "assistant", - "content": assistant_response - }) - await msg.update() - - # Append the reference section to the assistant's response - if user_session["context"]: - reference_section = "\n\nReferences:\n" - for ref, data in user_session["context"].items(): - reference_section += f"[{ref.split('_')[1]}]: {data['url']}\n" - msg.content += reference_section - await msg.update() - ``` - -7. **Handle Audio Input** - - ```python - @cl.on_audio_chunk - async def on_audio_chunk(chunk: cl.AudioChunk): - if chunk.isStart: - buffer = BytesIO() - buffer.name = f"input_audio.{chunk.mimeType.split('/')[1]}" - cl.user_session.set("audio_buffer", buffer) - cl.user_session.set("audio_mime_type", chunk.mimeType) - cl.user_session.get("audio_buffer").write(chunk.data) - - @cl.step(type="tool") - async def speech_to_text(audio_file): - response = await client.audio.transcriptions.create( - model="whisper-large-v3", file=audio_file - ) - return response.text - - @cl.on_audio_end - async def on_audio_end(elements: list[ElementBased]): - audio_buffer: BytesIO = cl.user_session.get("audio_buffer") - audio_buffer.seek(0) - audio_file = audio_buffer.read() - audio_mime_type: str = cl.user_session.get("audio_mime_type") - - start_time = time.time() - transcription = await speech_to_text((audio_buffer.name, audio_file, audio_mime_type)) - end_time = time.time() - print(f"Transcription took {end_time - start_time} seconds") - - user_msg = cl.Message(author="You", type="user_message", content=transcription) - await user_msg.send() - await on_message(user_msg) - ``` - -8. **Run the Chat Application** - - ```python - if __name__ == "__main__": - from chainlit.cli import run_chainlit - run_chainlit(__file__) - ``` - -## Explanation - -- **Libraries and Configuration**: We import necessary libraries, including `AsyncWebCrawler` from `crawl4ai`. -- **Utility Functions**: - - `extract_urls`: Uses regex to find URLs in messages. - - `crawl_urls`: An asynchronous function that uses `AsyncWebCrawler` to fetch content from multiple URLs concurrently. -- **Chat Start Event**: Initializes the chat session and sends a welcome message. -- **Message Handling**: - - Extracts URLs from user messages. - - Asynchronously crawls the URLs using `AsyncWebCrawler`. - - Updates chat history and context with crawled content. - - Generates a response using the LLM, incorporating the crawled context. -- **Audio Handling**: Captures, buffers, and transcribes audio input, then processes the transcription as text. -- **Running the Application**: Starts the Chainlit server for interaction with the assistant. - -## Key Improvements - -1. **Asynchronous Web Crawling**: Using `AsyncWebCrawler` allows for efficient, concurrent crawling of multiple URLs. -2. **Improved Context Management**: The assistant now maintains a context of crawled content, allowing for more informed responses. -3. **Dynamic Reference System**: The assistant can refer to specific sources in its responses and provide a reference section. -4. **Seamless Audio Integration**: The ability to handle audio inputs makes the assistant more versatile and user-friendly. - -This updated Research Assistant showcases how to create a powerful, interactive tool that can efficiently fetch and process web content, handle various input types, and provide informed responses based on the gathered information. \ No newline at end of file diff --git a/docs/md_v1/examples/summarization.md b/docs/md_v1/examples/summarization.md deleted file mode 100644 index 12c18471..00000000 --- a/docs/md_v1/examples/summarization.md +++ /dev/null @@ -1,153 +0,0 @@ -# Summarization Example with AsyncWebCrawler - -This example demonstrates how to use Crawl4AI's `AsyncWebCrawler` to extract a summary from a web page asynchronously. The goal is to obtain the title, a detailed summary, a brief summary, and a list of keywords from the given page. - -## Step-by-Step Guide - -1. **Import Necessary Modules** - - First, import the necessary modules and classes: - - ```python - import os - import json - import asyncio - from crawl4ai import AsyncWebCrawler - from crawl4ai.extraction_strategy import LLMExtractionStrategy - from crawl4ai.chunking_strategy import RegexChunking - from pydantic import BaseModel, Field - ``` - -2. **Define the URL to be Crawled** - - Set the URL of the web page you want to summarize: - - ```python - url = 'https://marketplace.visualstudio.com/items?itemName=Unclecode.groqopilot' - ``` - -3. **Define the Data Model** - - Use Pydantic to define the structure of the extracted data: - - ```python - class PageSummary(BaseModel): - title: str = Field(..., description="Title of the page.") - summary: str = Field(..., description="Summary of the page.") - brief_summary: str = Field(..., description="Brief summary of the page.") - keywords: list = Field(..., description="Keywords assigned to the page.") - ``` - -4. **Create the Extraction Strategy** - - Set up the `LLMExtractionStrategy` with the necessary parameters: - - ```python - extraction_strategy = LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv('OPENAI_API_KEY'), - schema=PageSummary.model_json_schema(), - extraction_type="schema", - apply_chunking=False, - instruction=( - "From the crawled content, extract the following details: " - "1. Title of the page " - "2. Summary of the page, which is a detailed summary " - "3. Brief summary of the page, which is a paragraph text " - "4. Keywords assigned to the page, which is a list of keywords. " - 'The extracted JSON format should look like this: ' - '{ "title": "Page Title", "summary": "Detailed summary of the page.", ' - '"brief_summary": "Brief summary in a paragraph.", "keywords": ["keyword1", "keyword2", "keyword3"] }' - ) - ) - ``` - -5. **Define the Async Crawl Function** - - Create an asynchronous function to run the crawler: - - ```python - async def crawl_and_summarize(url): - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url=url, - word_count_threshold=1, - extraction_strategy=extraction_strategy, - chunking_strategy=RegexChunking(), - bypass_cache=True, - ) - return result - ``` - -6. **Run the Crawler and Process Results** - - Use asyncio to run the crawler and process the results: - - ```python - async def main(): - result = await crawl_and_summarize(url) - - if result.success: - page_summary = json.loads(result.extracted_content) - print("Extracted Page Summary:") - print(json.dumps(page_summary, indent=2)) - - # Save the extracted data - with open(".data/page_summary.json", "w", encoding="utf-8") as f: - json.dump(page_summary, f, indent=2) - print("Page summary saved to .data/page_summary.json") - else: - print(f"Failed to crawl and summarize the page. Error: {result.error_message}") - - # Run the async main function - asyncio.run(main()) - ``` - -## Explanation - -- **Importing Modules**: We import the necessary modules, including `AsyncWebCrawler` and `LLMExtractionStrategy` from Crawl4AI. -- **URL Definition**: We set the URL of the web page to crawl and summarize. -- **Data Model Definition**: We define the structure of the data to extract using Pydantic's `BaseModel`. -- **Extraction Strategy Setup**: We create an instance of `LLMExtractionStrategy` with the schema and detailed instructions for the extraction process. -- **Async Crawl Function**: We define an asynchronous function `crawl_and_summarize` that uses `AsyncWebCrawler` to perform the crawling and extraction. -- **Main Execution**: In the `main` function, we run the crawler, process the results, and save the extracted data. - -## Advanced Usage: Crawling Multiple URLs - -To demonstrate the power of `AsyncWebCrawler`, here's how you can summarize multiple pages concurrently: - -```python -async def crawl_multiple_urls(urls): - async with AsyncWebCrawler(verbose=True) as crawler: - tasks = [crawler.arun( - url=url, - word_count_threshold=1, - extraction_strategy=extraction_strategy, - chunking_strategy=RegexChunking(), - bypass_cache=True - ) for url in urls] - results = await asyncio.gather(*tasks) - return results - -async def main(): - urls = [ - 'https://marketplace.visualstudio.com/items?itemName=Unclecode.groqopilot', - 'https://marketplace.visualstudio.com/items?itemName=GitHub.copilot', - 'https://marketplace.visualstudio.com/items?itemName=ms-python.python' - ] - results = await crawl_multiple_urls(urls) - - for i, result in enumerate(results): - if result.success: - page_summary = json.loads(result.extracted_content) - print(f"\nSummary for URL {i+1}:") - print(json.dumps(page_summary, indent=2)) - else: - print(f"\nFailed to summarize URL {i+1}. Error: {result.error_message}") - -asyncio.run(main()) -``` - -This advanced example shows how to use `AsyncWebCrawler` to efficiently summarize multiple web pages concurrently, significantly reducing the total processing time compared to sequential crawling. - -By leveraging the asynchronous capabilities of Crawl4AI, you can perform advanced web crawling and data extraction tasks with improved efficiency and scalability. \ No newline at end of file diff --git a/docs/md_v1/full_details/advanced_features.md b/docs/md_v1/full_details/advanced_features.md deleted file mode 100644 index 944f6e0d..00000000 --- a/docs/md_v1/full_details/advanced_features.md +++ /dev/null @@ -1,138 +0,0 @@ -# Advanced Features - -Crawl4AI offers a range of advanced features that allow you to fine-tune your web crawling and data extraction process. This section will cover some of these advanced features, including taking screenshots, extracting media and links, customizing the user agent, using custom hooks, and leveraging CSS selectors. - -## Taking Screenshots 📸 - -One of the cool features of Crawl4AI is the ability to take screenshots of the web pages you're crawling. This can be particularly useful for visual verification or for capturing the state of dynamic content. - -Here's how you can take a screenshot: - -```python -from crawl4ai import WebCrawler -import base64 - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Run the crawler with the screenshot parameter -result = crawler.run(url="https://www.nbcnews.com/business", screenshot=True) - -# Save the screenshot to a file -with open("screenshot.png", "wb") as f: - f.write(base64.b64decode(result.screenshot)) - -print("Screenshot saved to 'screenshot.png'!") -``` - -In this example, we create a `WebCrawler` instance, warm it up, and then run it with the `screenshot` parameter set to `True`. The screenshot is saved as a base64 encoded string in the result, which we then decode and save as a PNG file. - -## Extracting Media and Links 🎨🔗 - -Crawl4AI can extract all media tags (images, audio, and video) and links (both internal and external) from a web page. This feature is useful for collecting multimedia content or analyzing link structures. - -Here's an example: - -```python -from crawl4ai import WebCrawler - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Run the crawler -result = crawler.run(url="https://www.nbcnews.com/business") - -print("Extracted media:", result.media) -print("Extracted links:", result.links) -``` - -In this example, the `result` object contains dictionaries for media and links, which you can access and use as needed. - -## Customizing the User Agent 🕵️‍♂️ - -Crawl4AI allows you to set a custom user agent for your HTTP requests. This can help you avoid detection by web servers or simulate different browsing environments. - -Here's how to set a custom user agent: - -```python -from crawl4ai import WebCrawler - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Run the crawler with a custom user agent -result = crawler.run(url="https://www.nbcnews.com/business", user_agent="Mozilla/5.0 (compatible; MyCrawler/1.0)") - -print("Crawl result:", result) -``` - -In this example, we specify a custom user agent string when running the crawler. - -## Using Custom Hooks 🪝 - -Hooks are a powerful feature in Crawl4AI that allow you to customize the crawling process at various stages. You can define hooks for actions such as driver initialization, before and after URL fetching, and before returning the HTML. - -Here's an example of using hooks: - -```python -from crawl4ai import WebCrawler -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -# Define the hooks -def on_driver_created(driver): - driver.maximize_window() - driver.get('https://example.com/login') - WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.NAME, 'username'))).send_keys('testuser') - driver.find_element(By.NAME, 'password').send_keys('password123') - driver.find_element(By.NAME, 'login').click() - return driver - -def before_get_url(driver): - driver.execute_cdp_cmd('Network.setExtraHTTPHeaders', {'headers': {'X-Test-Header': 'test'}}) - return driver - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Set the hooks -crawler.set_hook('on_driver_created', on_driver_created) -crawler.set_hook('before_get_url', before_get_url) - -# Run the crawler -result = crawler.run(url="https://example.com") - -print("Crawl result:", result) -``` - -In this example, we define hooks to handle driver initialization and custom headers before fetching the URL. - -## Using CSS Selectors 🎯 - -CSS selectors allow you to target specific elements on a web page for extraction. This can be useful for scraping structured content, such as articles or product details. - -Here's an example of using a CSS selector: - -```python -from crawl4ai import WebCrawler - -# Create the WebCrawler instance -crawler = WebCrawler() -crawler.warmup() - -# Run the crawler with a CSS selector to extract only H2 tags -result = crawler.run(url="https://www.nbcnews.com/business", css_selector="h2") - -print("Extracted H2 tags:", result.extracted_content) -``` - -In this example, we use the `css_selector` parameter to extract only the H2 tags from the web page. - ---- - -With these advanced features, you can leverage Crawl4AI to perform sophisticated web crawling and data extraction tasks. Whether you need to take screenshots, extract specific elements, customize the crawling process, or set custom headers, Crawl4AI provides the flexibility and power to meet your needs. Happy crawling! 🕷️🚀 diff --git a/docs/md_v1/full_details/advanced_jsoncss_extraction.md b/docs/md_v1/full_details/advanced_jsoncss_extraction.md deleted file mode 100644 index 393b79a5..00000000 --- a/docs/md_v1/full_details/advanced_jsoncss_extraction.md +++ /dev/null @@ -1,282 +0,0 @@ -# Advanced Usage of JsonCssExtractionStrategy - -While the basic usage of JsonCssExtractionStrategy is powerful for simple structures, its true potential shines when dealing with complex, nested HTML structures. This section will explore advanced usage scenarios, demonstrating how to extract nested objects, lists, and nested lists. - -## Hypothetical Website Example - -Let's consider a hypothetical e-commerce website that displays product categories, each containing multiple products. Each product has details, reviews, and related items. This complex structure will allow us to demonstrate various advanced features of JsonCssExtractionStrategy. - -Assume the HTML structure looks something like this: - -```html -
-

Electronics

-
-

Smartphone X

-

$999

-
- TechCorp - X-2000 -
-
    -
  • 5G capable
  • -
  • 6.5" OLED screen
  • -
  • 128GB storage
  • -
-
-
- John D. - 4.5 -

Great phone, love the camera!

-
-
- Jane S. - 5 -

Best smartphone I've ever owned.

-
-
- -
- -
-``` - -Now, let's create a schema to extract this complex structure: - -```python -schema = { - "name": "E-commerce Product Catalog", - "baseSelector": "div.category", - "fields": [ - { - "name": "category_name", - "selector": "h2.category-name", - "type": "text" - }, - { - "name": "products", - "selector": "div.product", - "type": "nested_list", - "fields": [ - { - "name": "name", - "selector": "h3.product-name", - "type": "text" - }, - { - "name": "price", - "selector": "p.product-price", - "type": "text" - }, - { - "name": "details", - "selector": "div.product-details", - "type": "nested", - "fields": [ - { - "name": "brand", - "selector": "span.brand", - "type": "text" - }, - { - "name": "model", - "selector": "span.model", - "type": "text" - } - ] - }, - { - "name": "features", - "selector": "ul.product-features li", - "type": "list", - "fields": [ - { - "name": "feature", - "type": "text" - } - ] - }, - { - "name": "reviews", - "selector": "div.review", - "type": "nested_list", - "fields": [ - { - "name": "reviewer", - "selector": "span.reviewer", - "type": "text" - }, - { - "name": "rating", - "selector": "span.rating", - "type": "text" - }, - { - "name": "comment", - "selector": "p.review-text", - "type": "text" - } - ] - }, - { - "name": "related_products", - "selector": "ul.related-products li", - "type": "list", - "fields": [ - { - "name": "name", - "selector": "span.related-name", - "type": "text" - }, - { - "name": "price", - "selector": "span.related-price", - "type": "text" - } - ] - } - ] - } - ] -} -``` - -This schema demonstrates several advanced features: - -1. **Nested Objects**: The `details` field is a nested object within each product. -2. **Simple Lists**: The `features` field is a simple list of text items. -3. **Nested Lists**: The `products` field is a nested list, where each item is a complex object. -4. **Lists of Objects**: The `reviews` and `related_products` fields are lists of objects. - -Let's break down the key concepts: - -### Nested Objects - -To create a nested object, use `"type": "nested"` and provide a `fields` array for the nested structure: - -```python -{ - "name": "details", - "selector": "div.product-details", - "type": "nested", - "fields": [ - { - "name": "brand", - "selector": "span.brand", - "type": "text" - }, - { - "name": "model", - "selector": "span.model", - "type": "text" - } - ] -} -``` - -### Simple Lists - -For a simple list of identical items, use `"type": "list"`: - -```python -{ - "name": "features", - "selector": "ul.product-features li", - "type": "list", - "fields": [ - { - "name": "feature", - "type": "text" - } - ] -} -``` - -### Nested Lists - -For a list of complex objects, use `"type": "nested_list"`: - -```python -{ - "name": "products", - "selector": "div.product", - "type": "nested_list", - "fields": [ - // ... fields for each product - ] -} -``` - -### Lists of Objects - -Similar to nested lists, but typically used for simpler objects within the list: - -```python -{ - "name": "related_products", - "selector": "ul.related-products li", - "type": "list", - "fields": [ - { - "name": "name", - "selector": "span.related-name", - "type": "text" - }, - { - "name": "price", - "selector": "span.related-price", - "type": "text" - } - ] -} -``` - -## Using the Advanced Schema - -To use this advanced schema with AsyncWebCrawler: - -```python -import json -import asyncio -from crawl4ai import AsyncWebCrawler -from crawl4ai.extraction_strategy import JsonCssExtractionStrategy - -async def extract_complex_product_data(): - extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) - - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://gist.githubusercontent.com/githubusercontent/2d7b8ba3cd8ab6cf3c8da771ddb36878/raw/1ae2f90c6861ce7dd84cc50d3df9920dee5e1fd2/sample_ecommerce.html", - extraction_strategy=extraction_strategy, - bypass_cache=True, - ) - - assert result.success, "Failed to crawl the page" - - product_data = json.loads(result.extracted_content) - print(json.dumps(product_data, indent=2)) - -asyncio.run(extract_complex_product_data()) -``` - -This will produce a structured JSON output that captures the complex hierarchy of the product catalog, including nested objects, lists, and nested lists. - -## Tips for Advanced Usage - -1. **Start Simple**: Begin with a basic schema and gradually add complexity. -2. **Test Incrementally**: Test each part of your schema separately before combining them. -3. **Use Chrome DevTools**: The Element Inspector is invaluable for identifying the correct selectors. -4. **Handle Missing Data**: Use the `default` key in your field definitions to handle cases where data might be missing. -5. **Leverage Transforms**: Use the `transform` key to clean or format extracted data (e.g., converting prices to numbers). -6. **Consider Performance**: Very complex schemas might slow down extraction. Balance complexity with performance needs. - -By mastering these advanced techniques, you can use JsonCssExtractionStrategy to extract highly structured data from even the most complex web pages, making it a powerful tool for web scraping and data analysis tasks. \ No newline at end of file diff --git a/docs/md_v1/full_details/chunking_strategies.md b/docs/md_v1/full_details/chunking_strategies.md deleted file mode 100644 index f429310f..00000000 --- a/docs/md_v1/full_details/chunking_strategies.md +++ /dev/null @@ -1,133 +0,0 @@ -## Chunking Strategies 📚 - -Crawl4AI provides several powerful chunking strategies to divide text into manageable parts for further processing. Each strategy has unique characteristics and is suitable for different scenarios. Let's explore them one by one. - -### RegexChunking - -`RegexChunking` splits text using regular expressions. This is ideal for creating chunks based on specific patterns like paragraphs or sentences. - -#### When to Use -- Great for structured text with consistent delimiters. -- Suitable for documents where specific patterns (e.g., double newlines, periods) indicate logical chunks. - -#### Parameters -- `patterns` (list, optional): Regular expressions used to split the text. Default is to split by double newlines (`['\n\n']`). - -#### Example -```python -from crawl4ai.chunking_strategy import RegexChunking - -# Define patterns for splitting text -patterns = [r'\n\n', r'\. '] -chunker = RegexChunking(patterns=patterns) - -# Sample text -text = "This is a sample text. It will be split into chunks.\n\nThis is another paragraph." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -### NlpSentenceChunking - -`NlpSentenceChunking` uses NLP models to split text into sentences, ensuring accurate sentence boundaries. - -#### When to Use -- Ideal for texts where sentence boundaries are crucial. -- Useful for creating chunks that preserve grammatical structures. - -#### Parameters -- None. - -#### Example -```python -from crawl4ai.chunking_strategy import NlpSentenceChunking - -chunker = NlpSentenceChunking() - -# Sample text -text = "This is a sample text. It will be split into sentences. Here's another sentence." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -### TopicSegmentationChunking - -`TopicSegmentationChunking` employs the TextTiling algorithm to segment text into topic-based chunks. This method identifies thematic boundaries. - -#### When to Use -- Perfect for long documents with distinct topics. -- Useful when preserving topic continuity is more important than maintaining text order. - -#### Parameters -- `num_keywords` (int, optional): Number of keywords for each topic segment. Default is `3`. - -#### Example -```python -from crawl4ai.chunking_strategy import TopicSegmentationChunking - -chunker = TopicSegmentationChunking(num_keywords=3) - -# Sample text -text = "This document contains several topics. Topic one discusses AI. Topic two covers machine learning." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -### FixedLengthWordChunking - -`FixedLengthWordChunking` splits text into chunks based on a fixed number of words. This ensures each chunk has approximately the same length. - -#### When to Use -- Suitable for processing large texts where uniform chunk size is important. -- Useful when the number of words per chunk needs to be controlled. - -#### Parameters -- `chunk_size` (int, optional): Number of words per chunk. Default is `100`. - -#### Example -```python -from crawl4ai.chunking_strategy import FixedLengthWordChunking - -chunker = FixedLengthWordChunking(chunk_size=10) - -# Sample text -text = "This is a sample text. It will be split into chunks of fixed length." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -### SlidingWindowChunking - -`SlidingWindowChunking` uses a sliding window approach to create overlapping chunks. Each chunk has a fixed length, and the window slides by a specified step size. - -#### When to Use -- Ideal for creating overlapping chunks to preserve context. -- Useful for tasks where context from adjacent chunks is needed. - -#### Parameters -- `window_size` (int, optional): Number of words in each chunk. Default is `100`. -- `step` (int, optional): Number of words to slide the window. Default is `50`. - -#### Example -```python -from crawl4ai.chunking_strategy import SlidingWindowChunking - -chunker = SlidingWindowChunking(window_size=10, step=5) - -# Sample text -text = "This is a sample text. It will be split using a sliding window approach to preserve context." - -# Chunk the text -chunks = chunker.chunk(text) -print(chunks) -``` - -With these chunking strategies, you can choose the best method to divide your text based on your specific needs. Whether you need precise sentence boundaries, topic-based segmentation, or uniform chunk sizes, Crawl4AI has you covered. Happy chunking! 📝✨ diff --git a/docs/md_v1/full_details/crawl_request_parameters.md b/docs/md_v1/full_details/crawl_request_parameters.md deleted file mode 100644 index 896097de..00000000 --- a/docs/md_v1/full_details/crawl_request_parameters.md +++ /dev/null @@ -1,179 +0,0 @@ -# Crawl Request Parameters for AsyncWebCrawler - -The `arun` method in Crawl4AI's `AsyncWebCrawler` is designed to be highly configurable, allowing you to customize the crawling and extraction process to suit your needs. Below are the parameters you can use with the `arun` method, along with their descriptions, possible values, and examples. - -## Parameters - -### url (str) -**Description:** The URL of the webpage to crawl. -**Required:** Yes -**Example:** -```python -url = "https://www.nbcnews.com/business" -``` - -### word_count_threshold (int) -**Description:** The minimum number of words a block must contain to be considered meaningful. The default value is defined by `MIN_WORD_THRESHOLD`. -**Required:** No -**Default Value:** `MIN_WORD_THRESHOLD` -**Example:** -```python -word_count_threshold = 10 -``` - -### extraction_strategy (ExtractionStrategy) -**Description:** The strategy to use for extracting content from the HTML. It must be an instance of `ExtractionStrategy`. If not provided, the default is `NoExtractionStrategy`. -**Required:** No -**Default Value:** `NoExtractionStrategy()` -**Example:** -```python -extraction_strategy = CosineStrategy(semantic_filter="finance") -``` - -### chunking_strategy (ChunkingStrategy) -**Description:** The strategy to use for chunking the text before processing. It must be an instance of `ChunkingStrategy`. The default value is `RegexChunking()`. -**Required:** No -**Default Value:** `RegexChunking()` -**Example:** -```python -chunking_strategy = NlpSentenceChunking() -``` - -### bypass_cache (bool) -**Description:** Whether to force a fresh crawl even if the URL has been previously crawled. The default value is `False`. -**Required:** No -**Default Value:** `False` -**Example:** -```python -bypass_cache = True -``` - -### css_selector (str) -**Description:** The CSS selector to target specific parts of the HTML for extraction. If not provided, the entire HTML will be processed. -**Required:** No -**Default Value:** `None` -**Example:** -```python -css_selector = "div.article-content" -``` - -### screenshot (bool) -**Description:** Whether to take screenshots of the page. The default value is `False`. -**Required:** No -**Default Value:** `False` -**Example:** -```python -screenshot = True -``` - -### user_agent (str) -**Description:** The user agent to use for the HTTP requests. If not provided, a default user agent will be used. -**Required:** No -**Default Value:** `None` -**Example:** -```python -user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3" -``` - -### verbose (bool) -**Description:** Whether to enable verbose logging. The default value is `True`. -**Required:** No -**Default Value:** `True` -**Example:** -```python -verbose = True -``` - -### **kwargs -Additional keyword arguments that can be passed to customize the crawling process further. Some notable options include: - -- **only_text (bool):** Whether to extract only text content, excluding HTML tags. Default is `False`. -- **session_id (str):** A unique identifier for the crawling session. This is useful for maintaining state across multiple requests. -- **js_code (str or list):** JavaScript code to be executed on the page before extraction. -- **wait_for (str):** A CSS selector or JavaScript function to wait for before considering the page load complete. - -**Example:** -```python -result = await crawler.arun( - url="https://www.nbcnews.com/business", - css_selector="p", - only_text=True, - session_id="unique_session_123", - js_code="window.scrollTo(0, document.body.scrollHeight);", - wait_for="article.main-article" -) -``` - -## Example Usage - -Here's an example of how to use the `arun` method with various parameters: - -```python -import asyncio -from crawl4ai import AsyncWebCrawler -from crawl4ai.extraction_strategy import CosineStrategy -from crawl4ai.chunking_strategy import NlpSentenceChunking - -async def main(): - # Create the AsyncWebCrawler instance - async with AsyncWebCrawler(verbose=True) as crawler: - # Run the crawler with custom parameters - result = await crawler.arun( - url="https://www.nbcnews.com/business", - word_count_threshold=10, - extraction_strategy=CosineStrategy(semantic_filter="finance"), - chunking_strategy=NlpSentenceChunking(), - bypass_cache=True, - css_selector="div.article-content", - screenshot=True, - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", - verbose=True, - only_text=True, - session_id="business_news_session", - js_code="window.scrollTo(0, document.body.scrollHeight);", - wait_for="footer" - ) - - print(result) - -# Run the async function -asyncio.run(main()) -``` - -This example demonstrates how to configure various parameters to customize the crawling and extraction process using the asynchronous version of Crawl4AI. - -## Additional Asynchronous Methods - -The `AsyncWebCrawler` class also provides other useful asynchronous methods: - -### arun_many -**Description:** Crawl multiple URLs concurrently. -**Example:** -```python -urls = ["https://example1.com", "https://example2.com", "https://example3.com"] -results = await crawler.arun_many(urls, word_count_threshold=10, bypass_cache=True) -``` - -### aclear_cache -**Description:** Clear the crawler's cache. -**Example:** -```python -await crawler.aclear_cache() -``` - -### aflush_cache -**Description:** Completely flush the crawler's cache. -**Example:** -```python -await crawler.aflush_cache() -``` - -### aget_cache_size -**Description:** Get the current size of the cache. -**Example:** -```python -cache_size = await crawler.aget_cache_size() -print(f"Current cache size: {cache_size}") -``` - -These asynchronous methods allow for efficient and flexible use of the AsyncWebCrawler in various scenarios. \ No newline at end of file diff --git a/docs/md_v1/full_details/crawl_result_class.md b/docs/md_v1/full_details/crawl_result_class.md deleted file mode 100644 index e2c545fe..00000000 --- a/docs/md_v1/full_details/crawl_result_class.md +++ /dev/null @@ -1,104 +0,0 @@ -# Crawl Result - -The `CrawlResult` class is the heart of Crawl4AI's output, encapsulating all the data extracted from a crawling session. This class contains various fields that store the results of the web crawling and extraction process. Let's break down each field and see what it holds. 🎉 - -## Class Definition - -```python -from pydantic import BaseModel -from typing import Dict, List, Optional - -class CrawlResult(BaseModel): - url: str - html: str - success: bool - cleaned_html: Optional[str] = None - media: Dict[str, List[Dict]] = {} - links: Dict[str, List[Dict]] = {} - screenshot: Optional[str] = None - markdown: Optional[str] = None - extracted_content: Optional[str] = None - metadata: Optional[dict] = None - error_message: Optional[str] = None - session_id: Optional[str] = None - responser_headers: Optional[dict] = None - status_code: Optional[int] = None -``` - -## Fields Explanation - -### `url: str` -The URL that was crawled. This field simply stores the URL of the web page that was processed. - -### `html: str` -The raw HTML content of the web page. This is the unprocessed HTML source as retrieved by the crawler. - -### `success: bool` -A flag indicating whether the crawling and extraction were successful. If any error occurs during the process, this will be `False`. - -### `cleaned_html: Optional[str]` -The cleaned HTML content of the web page. This field holds the HTML after removing unwanted tags like ` diff --git a/docs/md_v1/introduction.md b/docs/md_v1/introduction.md deleted file mode 100644 index 6d1ad56b..00000000 --- a/docs/md_v1/introduction.md +++ /dev/null @@ -1,29 +0,0 @@ -# Introduction - -Welcome to the documentation for Crawl4AI v0.2.5! 🕷️🤖 - -Crawl4AI is designed to simplify the process of crawling web pages and extracting useful information for large language models (LLMs) and AI applications. Whether you're using it as a REST API, a Python library, or through a Google Colab notebook, Crawl4AI provides powerful features to make web data extraction easier and more efficient. - -## Key Features ✨ - -- **🆓 Completely Free and Open-Source**: Crawl4AI is free to use and open-source, making it accessible for everyone. -- **🤖 LLM-Friendly Output Formats**: Supports JSON, cleaned HTML, and markdown formats. -- **🌍 Concurrent Crawling**: Crawl multiple URLs simultaneously to save time. -- **🎨 Media Extraction**: Extract all media tags including images, audio, and video. -- **🔗 Link Extraction**: Extract all external and internal links from web pages. -- **📚 Metadata Extraction**: Extract metadata from web pages for additional context. -- **🔄 Custom Hooks**: Define custom hooks for authentication, headers, and page modifications before crawling. -- **🕵️ User Agent Support**: Customize the user agent for HTTP requests. -- **🖼️ Screenshot Capability**: Take screenshots of web pages during crawling. -- **📜 JavaScript Execution**: Execute custom JavaScripts before crawling. -- **📚 Advanced Chunking and Extraction Strategies**: Utilize topic-based, regex, sentence chunking, cosine clustering, and LLM extraction strategies. -- **🎯 CSS Selector Support**: Extract specific content using CSS selectors. -- **📝 Instruction/Keyword Refinement**: Pass instructions or keywords to refine the extraction process. - -Check the [Changelog](https://github.com/unclecode/crawl4ai/blob/main/CHANGELOG.md) for more details. - -## Power and Simplicity of Crawl4AI 🚀 - -Crawl4AI provides an easy way to crawl and extract data from web pages without installing any library. You can use the REST API on our server or run the local server on your machine. For more advanced control, use the Python library to customize your crawling and extraction strategies. - -Explore the documentation to learn more about the features, installation process, usage examples, and how to contribute to Crawl4AI. Let's make the web more accessible and useful for AI applications! 💪🌐🤖 diff --git a/docs/md_v1/mkdocs.yml b/docs/md_v1/mkdocs.yml deleted file mode 100644 index d56ddfec..00000000 --- a/docs/md_v1/mkdocs.yml +++ /dev/null @@ -1,45 +0,0 @@ -site_name: Crawl4AI Documentation -site_description: 🔥🕷️ Crawl4AI, Open-source LLM Friendly Web Crawler & Scrapper -site_url: https://docs.crawl4ai.com -repo_url: https://github.com/unclecode/crawl4ai -repo_name: unclecode/crawl4ai -docs_dir: docs/md -nav: - - Home: index.md - - First Steps: - - Introduction: introduction.md - - Installation: installation.md - - Quick Start: quickstart.md - - Examples: - - Intro: examples/index.md - - Structured Data Extraction: examples/json_css_extraction.md - - LLM Extraction: examples/llm_extraction.md - - JS Execution & CSS Filtering: examples/js_execution_css_filtering.md - - Hooks & Auth: examples/hooks_auth.md - - Summarization: examples/summarization.md - - Research Assistant: examples/research_assistant.md - - Full Details of Using Crawler: - - Crawl Request Parameters: full_details/crawl_request_parameters.md - - Crawl Result Class: full_details/crawl_result_class.md - - Session Based Crawling: full_details/session_based_crawling.md - - Advanced Features: full_details/advanced_features.md - - Advanced JsonCssExtraction: full_details/advanced_jsoncss_extraction.md - - Chunking Strategies: full_details/chunking_strategies.md - - Extraction Strategies: full_details/extraction_strategies.md - - Miscellaneous: - - Change Log: changelog.md - - Contact: contact.md - -theme: - name: terminal - palette: dark - -# Add the css/extra.css -extra_css: - - assets/styles.css - - assets/highlight.css - - assets/dmvendor.css - -extra_javascript: - - assets/highlight.min.js - - assets/highlight_init.js diff --git a/docs/md_v1/quickstart.md b/docs/md_v1/quickstart.md deleted file mode 100644 index ef79faca..00000000 --- a/docs/md_v1/quickstart.md +++ /dev/null @@ -1,285 +0,0 @@ -# Quick Start Guide 🚀 - -Welcome to the Crawl4AI Quickstart Guide! In this tutorial, we'll walk you through the basic usage of Crawl4AI with a friendly and humorous tone. We'll cover everything from basic usage to advanced features like chunking and extraction strategies, all with the power of asynchronous programming. Let's dive in! 🌟 - -## Getting Started 🛠️ - -First, let's import the necessary modules and create an instance of `AsyncWebCrawler`. We'll use an async context manager, which handles the setup and teardown of the crawler for us. - -```python -import asyncio -from crawl4ai import AsyncWebCrawler - -async def main(): - async with AsyncWebCrawler(verbose=True) as crawler: - # We'll add our crawling code here - pass - -if __name__ == "__main__": - asyncio.run(main()) -``` - -### Basic Usage - -Simply provide a URL and let Crawl4AI do the magic! - -```python -async def main(): - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun(url="https://www.nbcnews.com/business") - print(f"Basic crawl result: {result.markdown[:500]}") # Print first 500 characters - -asyncio.run(main()) -``` - -### Taking Screenshots 📸 - -Let's take a screenshot of the page! - -```python -import base64 - -async def main(): - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun(url="https://www.nbcnews.com/business", screenshot=True) - with open("screenshot.png", "wb") as f: - f.write(base64.b64decode(result.screenshot)) - print("Screenshot saved to 'screenshot.png'!") - -asyncio.run(main()) -``` - -### Understanding Parameters 🧠 - -By default, Crawl4AI caches the results of your crawls. This means that subsequent crawls of the same URL will be much faster! Let's see this in action. - -```python -async def main(): - async with AsyncWebCrawler(verbose=True) as crawler: - # First crawl (caches the result) - result1 = await crawler.arun(url="https://www.nbcnews.com/business") - print(f"First crawl result: {result1.markdown[:100]}...") - - # Force to crawl again - result2 = await crawler.arun(url="https://www.nbcnews.com/business", bypass_cache=True) - print(f"Second crawl result: {result2.markdown[:100]}...") - -asyncio.run(main()) -``` - -### Adding a Chunking Strategy 🧩 - -Let's add a chunking strategy: `RegexChunking`! This strategy splits the text based on a given regex pattern. - -```python -from crawl4ai.chunking_strategy import RegexChunking - -async def main(): - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://www.nbcnews.com/business", - chunking_strategy=RegexChunking(patterns=["\n\n"]) - ) - print(f"RegexChunking result: {result.extracted_content[:200]}...") - -asyncio.run(main()) -``` - -### Adding an Extraction Strategy 🧠 - -Let's get smarter with an extraction strategy: `JsonCssExtractionStrategy`! This strategy extracts structured data from HTML using CSS selectors. - -```python -from crawl4ai.extraction_strategy import JsonCssExtractionStrategy -import json - -async def main(): - schema = { - "name": "News Articles", - "baseSelector": "article.tease-card", - "fields": [ - { - "name": "title", - "selector": "h2", - "type": "text", - }, - { - "name": "summary", - "selector": "div.tease-card__info", - "type": "text", - } - ], - } - - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://www.nbcnews.com/business", - extraction_strategy=JsonCssExtractionStrategy(schema, verbose=True) - ) - extracted_data = json.loads(result.extracted_content) - print(f"Extracted {len(extracted_data)} articles") - print(json.dumps(extracted_data[0], indent=2)) - -asyncio.run(main()) -``` - -### Using LLMExtractionStrategy 🤖 - -Time to bring in the big guns: `LLMExtractionStrategy`! This strategy uses a large language model to extract relevant information from the web page. - -```python -from crawl4ai.extraction_strategy import LLMExtractionStrategy -import os -from pydantic import BaseModel, Field - -class OpenAIModelFee(BaseModel): - model_name: str = Field(..., description="Name of the OpenAI model.") - input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") - output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") - -async def main(): - if not os.getenv("OPENAI_API_KEY"): - print("OpenAI API key not found. Skipping this example.") - return - - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://openai.com/api/pricing/", - word_count_threshold=1, - extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", # Or use open source model like "ollama/nemotron" - api_token=os.getenv("OPENAI_API_KEY"), # Pass "no-token" if using Ollama - schema=OpenAIModelFee.schema(), - extraction_type="schema", - instruction="""From the crawled content, extract all mentioned model names along with their fees for input and output tokens. - Do not miss any models in the entire content. One extracted model JSON format should look like this: - {"model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens"}.""", - ), - bypass_cache=True, - ) - print(result.extracted_content) - -asyncio.run(main()) -``` - -### Interactive Extraction 🖱️ - -Let's use JavaScript to interact with the page before extraction! - -```python -async def main(): - js_code = """ - const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); - loadMoreButton && loadMoreButton.click(); - """ - - wait_for = """() => { - return Array.from(document.querySelectorAll('article.tease-card')).length > 10; - }""" - - async with AsyncWebCrawler(verbose=True) as crawler: - result = await crawler.arun( - url="https://www.nbcnews.com/business", - js_code=js_code, - wait_for=wait_for, - css_selector="article.tease-card", - bypass_cache=True, - ) - print(f"JavaScript interaction result: {result.extracted_content[:500]}") - -asyncio.run(main()) -``` - -### Advanced Session-Based Crawling with Dynamic Content 🔄 - -In modern web applications, content is often loaded dynamically without changing the URL. This is common in single-page applications (SPAs) or websites using infinite scrolling. Traditional crawling methods that rely on URL changes won't work here. That's where Crawl4AI's advanced session-based crawling comes in handy! - -Here's what makes this approach powerful: - -1.**Session Preservation**: By using a `session_id`, we can maintain the state of our crawling session across multiple interactions with the page. This is crucial for navigating through dynamically loaded content. - -2.**Asynchronous JavaScript Execution**: We can execute custom JavaScript to trigger content loading or navigation. In this example, we'll click a "Load More" button to fetch the next page of commits. - -3.**Dynamic Content Waiting**: The `wait_for` parameter allows us to specify a condition that must be met before considering the page load complete. This ensures we don't extract data before the new content is fully loaded. - -Let's see how this works with a real-world example: crawling multiple pages of commits on a GitHub repository. The URL doesn't change as we load more commits, so we'll use these advanced techniques to navigate and extract data. - -```python -import json -from bs4 import BeautifulSoup -from crawl4ai import AsyncWebCrawler -from crawl4ai.extraction_strategy import JsonCssExtractionStrategy - -async def main(): - async with AsyncWebCrawler(verbose=True) as crawler: - url = "https://github.com/microsoft/TypeScript/commits/main" - session_id = "typescript_commits_session" - all_commits = [] - - js_next_page = """ - const button = document.querySelector('a[data-testid="pagination-next-button"]'); - if (button) button.click(); - """ - - wait_for = """() => { - const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4'); - if (commits.length === 0) return false; - const firstCommit = commits[0].textContent.trim(); - return firstCommit !== window.lastCommit; - }""" - - schema = { - "name": "Commit Extractor", - "baseSelector": "li.Box-sc-g0xbh4-0", - "fields": [ - { - "name": "title", - "selector": "h4.markdown-title", - "type": "text", - "transform": "strip", - }, - ], - } - extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) - - for page in range(3): # Crawl 3 pages - result = await crawler.arun( - url=url, - session_id=session_id, - css_selector="li.Box-sc-g0xbh4-0", - extraction_strategy=extraction_strategy, - js_code=js_next_page if page > 0 else None, - wait_for=wait_for if page > 0 else None, - js_only=page > 0, - bypass_cache=True, - headless=False, - ) - - assert result.success, f"Failed to crawl page {page + 1}" - - commits = json.loads(result.extracted_content) - all_commits.extend(commits) - - print(f"Page {page + 1}: Found {len(commits)} commits") - - await crawler.crawler_strategy.kill_session(session_id) - print(f"Successfully crawled {len(all_commits)} commits across 3 pages") - -asyncio.run(main()) -``` - -In this example, we're crawling multiple pages of commits from a GitHub repository. The URL doesn't change as we load more commits, so we use JavaScript to click the "Load More" button and a `wait_for` condition to ensure the new content is loaded before extraction. This powerful combination allows us to navigate and extract data from complex, dynamically-loaded web applications with ease! - -## Congratulations! 🎉 - -You've made it through the Crawl4AI Quickstart Guide! Now go forth and crawl the web asynchronously like a pro! 🕸️ - -Remember, these are just a few examples of what Crawl4AI can do. For more advanced usage, check out our other documentation pages: - -- [LLM Extraction](examples/llm_extraction.md) -- [JS Execution & CSS Filtering](examples/js_execution_css_filtering.md) -- [Hooks & Auth](examples/hooks_auth.md) -- [Summarization](examples/summarization.md) -- [Research Assistant](examples/research_assistant.md) - -Happy crawling! 🚀 \ No newline at end of file