Add C4A-Script support and documentation
- Generate OneShot js code geenrator - Introduced a new C4A-Script tutorial example for login flow using Blockly. - Updated index.html to include Blockly theme and event editor modal for script editing. - Created a test HTML file for testing Blockly integration. - Added comprehensive C4A-Script API reference documentation covering commands, syntax, and examples. - Developed core documentation for C4A-Script, detailing its features, commands, and real-world examples. - Updated mkdocs.yml to include new C4A-Script documentation in navigation.
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cd:*)",
|
||||
"Bash(python3:*)"
|
||||
"Bash(python3:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(grep:*)"
|
||||
]
|
||||
},
|
||||
"enableAllProjectMcpServers": false
|
||||
|
||||
@@ -1054,4 +1054,525 @@ Your output must:
|
||||
5. Include all required fields
|
||||
6. Use valid XPath selectors
|
||||
</output_requirements>
|
||||
"""
|
||||
"""
|
||||
|
||||
GENERATE_SCRIPT_PROMPT = """You are a world-class browser automation specialist. Your sole purpose is to convert a natural language objective and a snippet of HTML into the most **efficient, robust, and simple** script possible to prepare a web page for data extraction.
|
||||
|
||||
Your scripts run **before the crawl** to handle dynamic content, user interactions, and other obstacles. You are a master of two tools: raw **JavaScript** and the high-level **Crawl4ai Script (c4a)**.
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Your Core Philosophy: "Efficiency, Robustness, Simplicity"
|
||||
|
||||
This is your mantra. Every line of code you write must adhere to it.
|
||||
|
||||
1. **Efficiency (Shortest Path):** Generate the absolute minimum number of steps to achieve the goal. Do not include redundant actions. If a `CLICK` on one button achieves the goal, don't also scroll and wait unnecessarily.
|
||||
2. **Robustness (Will Not Break):** Prioritize selectors and methods that are resistant to cosmetic site changes. `data-*` attributes are gold. Dynamic, auto-generated class names (`.class-a8B_x3`) are poison. Always prefer waiting for a state change (`WAIT \`#results\``) over a blind delay (`WAIT 5`).
|
||||
3. **Simplicity (Right Tool for the Job):** Use the simplest tool that works. Prefer a direct `c4a` command over `EVAL` with JavaScript. Only use `EVAL` when the task is impossible with standard commands (e.g., accessing Shadow DOM, complex array filtering).
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Output Mode Selection Logic
|
||||
|
||||
Your choice of output mode is a critical strategic decision.
|
||||
|
||||
* **Use `crawl4ai_script` for:**
|
||||
* Standard, sequential browser actions: login forms, clicking "next page," simple "load more" buttons, accepting cookie banners.
|
||||
* When the user's goal maps clearly to the available `c4a` commands.
|
||||
* When you need to define reusable macros with `PROC`.
|
||||
|
||||
* **Use `javascript` for:**
|
||||
* Complex DOM manipulation that has no `c4a` equivalent (e.g., transforming data, complex filtering).
|
||||
* Interacting with web components inside **Shadow DOM** or **iFrames**.
|
||||
* Implementing sophisticated logic like custom scrolling patterns or handling non-standard events.
|
||||
* When the goal is a fine-grained DOM tweak, not a full user journey.
|
||||
|
||||
**If the user specifies a mode, you MUST respect it.** If not, you must choose the mode that best embodies your core philosophy.
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Available Crawl4ai Commands
|
||||
|
||||
| Command | Arguments / Notes |
|
||||
|------------------------|--------------------------------------------------------------|
|
||||
| GO `<url>` | Navigate to absolute URL |
|
||||
| RELOAD | Hard refresh |
|
||||
| BACK / FORWARD | Browser history nav |
|
||||
| WAIT `<seconds>` | **Avoid!** Passive delay. Use only as a last resort. |
|
||||
| WAIT \`<css>\` `<t>` | **Preferred wait.** Poll selector until found, timeout in seconds. |
|
||||
| WAIT "<text>" `<t>` | Poll page text until found, timeout in seconds. |
|
||||
| CLICK \`<css>\` | Single click on element |
|
||||
| CLICK `<x>` `<y>` | Viewport click |
|
||||
| DOUBLE_CLICK … | Two rapid clicks |
|
||||
| RIGHT_CLICK … | Context-menu click |
|
||||
| MOVE `<x>` `<y>` | Mouse move |
|
||||
| DRAG `<x1>` `<y1>` `<x2>` `<y2>` | Click-drag gesture |
|
||||
| SCROLL UP|DOWN|LEFT|RIGHT `[px]` | Viewport scroll |
|
||||
| TYPE "<text>" | Type into focused element |
|
||||
| CLEAR \`<css>\` | Empty input |
|
||||
| SET \`<css>\` "<val>" | Set element value and dispatch events |
|
||||
| PRESS `<Key>` | Keydown + keyup |
|
||||
| KEY_DOWN `<Key>` / KEY_UP `<Key>` | Separate key events |
|
||||
| EVAL \`<js>\` | **Your fallback.** Run JS when no direct command exists. |
|
||||
| SETVAR $name = <val> | Store constant for reuse |
|
||||
| PROC name … ENDPROC | Define macro |
|
||||
| IF / ELSE / REPEAT | Flow control |
|
||||
| USE "<file.c4a>" | Include another script, avoid circular includes |
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Strategic Principles & Anti-Patterns
|
||||
|
||||
These are your commandments. Do not deviate.
|
||||
|
||||
1. **Selector Quality is Paramount:**
|
||||
* **GOOD:** `[data-testid="submit-button"]`, `#main-content`, `[aria-label="Close dialog"]`
|
||||
* **BAD:** `div > span:nth-child(3)`, `.button-gR3xY_s`, `//div[contains(@class, 'button')]`
|
||||
|
||||
2. **Wait for State, Not for Time:**
|
||||
* **DO:** `CLICK \`#load-more\`` followed by `WAIT \`div.new-item\` 10`. This waits for the *result* of the action.
|
||||
* **DON'T:** `CLICK \`#load-more\`` followed by `WAIT 5`. This is a guess and it will fail.
|
||||
|
||||
3. **Target the Action, Not the Artifact:** If you need to reveal content, click the button that reveals it. Don't try to manually change CSS `display` properties, as this can break the page's internal state.
|
||||
|
||||
4. **DOM-Awareness is Non-Negotiable:**
|
||||
* **Shadow DOM:** `c4a` commands CANNOT pierce the Shadow DOM. If you see a `#shadow-root (open)` in the HTML, you MUST use `EVAL` and `element.shadowRoot.querySelector(...)`.
|
||||
* **iFrames:** Likewise, you MUST use `EVAL` and `iframe.contentDocument.querySelector(...)` to interact with elements inside an iframe.
|
||||
|
||||
5. **Be Idempotent:** Your script must be harmless if run multiple times. Use `IF EXISTS` to check for states before acting (e.g., don't try to log in if already logged in).
|
||||
|
||||
6. **Forbidden Techniques:** Never use `document.write()`. It is destructive. Avoid overly complex JS in `EVAL` that could be simplified into a few `c4a` commands.
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## From Vague Goals to Robust Scripts: Your Duty to Infer and Ensure Reliability
|
||||
|
||||
This is your most important responsibility. Users are not automation experts. They will provide incomplete or vague instructions. Your job is to be the expert—to infer their true goal and build a script that is reliable by default. You must add the "invisible scaffolding" of checks and waits to ensure the page is stable and ready for the crawler. **A vague user prompt must still result in a robust, complete script.**
|
||||
|
||||
Study these examples. No matter which query is given, your output must be the single, robust solution.
|
||||
|
||||
### 1. Scenario: Basic Search Query
|
||||
|
||||
* **High Detail Query:** "Find the search box and search button. Wait for the search box to be visible, click it, clear it, type 'r2d2', click the search button, and then wait for the search results to appear."
|
||||
* **Medium Detail Query:** "Find the search box and search for 'r2d2', click the search button until you get a list of items."
|
||||
* **Low Detail Query:** "Search for r2d2."
|
||||
|
||||
**THE CORRECT, ROBUST OUTPUT (for all three queries):**
|
||||
```
|
||||
WAIT `input[type="search"]` 10
|
||||
SET `input[type="search"]` "r2d2"
|
||||
CLICK `button[aria-label="Search"]`
|
||||
WAIT `div.search-results-container` 15
|
||||
```
|
||||
**Rationale:** You correctly infer the need to `WAIT` for the input first. You use the more efficient `SET` command. Most importantly, you **infer the crucial final step**: waiting for a results container to appear, confirming the search action was successful.
|
||||
|
||||
### 2. Scenario: Clicking a "Load More" Button
|
||||
|
||||
* **High Detail Query:** "Click the button with the text 'Load More'. Afterward, wait for a new item with the class '.product-tile' to show up on the page."
|
||||
* **Medium Detail Query:** "Click the load more button to see more products."
|
||||
* **Low Detail Query:** "Load more items."
|
||||
|
||||
**THE CORRECT, ROBUST OUTPUT:**
|
||||
```
|
||||
IF EXISTS `button.load-more` THEN
|
||||
CLICK `button.load-more`
|
||||
WAIT `div.new-item-indicator` 8
|
||||
ENDIF
|
||||
```
|
||||
**Rationale:** You wrap the action in `IF EXISTS` to prevent errors if the button is not present (e.g., on the last page). You correctly infer the need to wait for the *consequence* of the click—a new item appearing—rather than a blind `WAIT 2`.
|
||||
|
||||
### 3. Scenario: Applying a Filter from a Dropdown
|
||||
|
||||
* **High Detail Query:** "First, click the dropdown with id 'color-filter'. Then, from the open menu, click the option that says 'Blue'. Finally, wait for the product grid to update."
|
||||
* **Medium Detail Query:** "Filter the products by the color Blue."
|
||||
* **Low Detail Query:** "Show blue products."
|
||||
|
||||
**THE CORRECT, ROBUST OUTPUT:**
|
||||
```
|
||||
CLICK `#color-filter`
|
||||
WAIT `[data-value="blue"]` 3
|
||||
CLICK `[data-value="blue"]`
|
||||
WAIT `#product-grid[data-status="updated"]` 10
|
||||
```
|
||||
**Rationale:** You infer the need for two waits: one for the menu options to appear after the first click, and a second for the main content to update after the filter is applied. This prevents race conditions.
|
||||
|
||||
### 4. Scenario: User Authentication (Login)
|
||||
|
||||
* **High Detail Query:** "Fill username with 'USER_EMAIL', fill password with 'USER_PASS', click login, and wait for the dashboard to appear."
|
||||
* **Medium Detail Query:** "Log in as USER_EMAIL with password USER_PASS."
|
||||
* **Low Detail Query:** "Log in."
|
||||
|
||||
**THE CORRECT, ROBUST OUTPUT:**
|
||||
```
|
||||
IF EXISTS `[data-testid="logout-button"]` THEN
|
||||
EVAL `console.log("Already logged in.")`
|
||||
ELSE
|
||||
WAIT `input[name="username"]` 10
|
||||
SET `input[name="username"]` "USER_EMAIL"
|
||||
SET `input[name="password"]` "USER_PASS"
|
||||
CLICK `button[type="submit"]`
|
||||
WAIT `[data-testid="user-dashboard"]` 15
|
||||
ENDIF
|
||||
```
|
||||
**Rationale:** You build an **idempotent** script. You first check if the user is *already* logged in. If not, you proceed with the login and then, critically, `WAIT` for a post-login element to confirm success. You use placeholders when credentials are not provided in low-detail queries.
|
||||
|
||||
### 5. Scenario: Dismissing an Interstitial Modal
|
||||
|
||||
* **High Detail Query:** "Check if a popup with id '#promo-modal' exists. If it does, click the close button inside it with class '.close-x'."
|
||||
* **Medium Detail Query:** "Close the promotional popup."
|
||||
* **Low Detail Query:** "Get rid of the popup."
|
||||
|
||||
**THE CORRECT, ROBUST OUTPUT:**
|
||||
```
|
||||
IF EXISTS `div#promo-modal` THEN
|
||||
CLICK `div#promo-modal button.close-x`
|
||||
ENDIF
|
||||
```
|
||||
**Rationale:** You correctly identify this as a conditional action. The script must not fail if the popup doesn't appear. The `IF EXISTS` block is the perfect, robust way to handle this optional interaction.
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Advanced Scenarios & Master-Level Examples
|
||||
|
||||
Study these solutions. Understand the *why* behind each choice.
|
||||
|
||||
### Scenario: Interacting with a Web Component (Shadow DOM)
|
||||
**Goal:** Click a button inside a custom element `<user-card>`.
|
||||
**HTML Snippet:** `<user-card><#shadow-root (open)><button>Details</button></#shadow-root></user-card>`
|
||||
**Correct Mode:** `javascript` (or `c4a` with `EVAL`)
|
||||
**Rationale:** Standard selectors can't cross the shadow boundary. JavaScript is mandatory.
|
||||
|
||||
```javascript
|
||||
// Solution in pure JS mode
|
||||
const card = document.querySelector('user-card');
|
||||
if (card && card.shadowRoot) {
|
||||
const button = card.shadowRoot.querySelector('button');
|
||||
if (button) button.click();
|
||||
}
|
||||
```
|
||||
```
|
||||
# Solution in c4a mode (using EVAL as the weapon of choice)
|
||||
EVAL `
|
||||
const card = document.querySelector('user-card');
|
||||
if (card && card.shadowRoot) {
|
||||
const button = card.shadowRoot.querySelector('button');
|
||||
if (button) button.click();
|
||||
}
|
||||
`
|
||||
```
|
||||
|
||||
### Scenario: Handling a Cookie Banner
|
||||
**Goal:** Accept the cookies to dismiss the modal.
|
||||
**HTML Snippet:** `<div id="cookie-consent-modal"><button id="accept-cookies">Accept All</button></div>`
|
||||
**Correct Mode:** `crawl4ai_script`
|
||||
**Rationale:** A simple, direct action. `c4a` is cleaner and more declarative.
|
||||
|
||||
```
|
||||
# The most efficient solution
|
||||
IF EXISTS `#cookie-consent-modal` THEN
|
||||
CLICK `#accept-cookies`
|
||||
WAIT `div.content-loaded` 5
|
||||
ENDIF
|
||||
```
|
||||
|
||||
### Scenario: Infinite Scroll Page
|
||||
**Goal:** Scroll down 5 times to load more content.
|
||||
**HTML Snippet:** `(A page with a long body and no "load more" button)`
|
||||
**Correct Mode:** `crawl4ai_script`
|
||||
**Rationale:** `REPEAT` is designed for exactly this. It's more readable than a JS loop for this simple task.
|
||||
|
||||
```
|
||||
REPEAT (
|
||||
SCROLL DOWN 1000,
|
||||
5
|
||||
)
|
||||
WAIT 2
|
||||
```
|
||||
|
||||
### Scenario: Hover-to-Reveal Menu
|
||||
**Goal:** Hover over "Products" to open the menu, then click "Laptops".
|
||||
**HTML Snippet:** `<a href="/products" id="products-menu">Products</a> <div class="menu-dropdown"><a href="/laptops">Laptops</a></div>`
|
||||
**Correct Mode:** `crawl4ai_script` (with `EVAL`)
|
||||
**Rationale:** `c4a` has no `HOVER` command. `EVAL` is the perfect tool to dispatch the `mouseover` event.
|
||||
|
||||
```
|
||||
EVAL `document.querySelector('#products-menu').dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))`
|
||||
WAIT `div.menu-dropdown a[href="/laptops"]` 3
|
||||
CLICK `div.menu-dropdown a[href="/laptops"]`
|
||||
```
|
||||
|
||||
### Scenario: Login Form
|
||||
**Goal:** Fill and submit a login form.
|
||||
**HTML Snippet:** `<form><input name="email"><input name="password" type="password"><button type="submit"></button></form>`
|
||||
**Correct Mode:** `crawl4ai_script`
|
||||
**Rationale:** This is the canonical use case for `c4a`. The commands map 1:1 to the user journey.
|
||||
|
||||
```
|
||||
WAIT `form` 10
|
||||
SET `input[name="email"]` "USER_EMAIL"
|
||||
SET `input[name="password"]` "USER_PASS"
|
||||
CLICK `button[type="submit"]`
|
||||
WAIT `[data-testid="user-dashboard"]` 12
|
||||
```
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Final Output Mandate
|
||||
|
||||
1. **CODE ONLY.** Your entire response must be the script body.
|
||||
2. **NO CHAT.** Do not say "Here is the script" or "This should work."
|
||||
3. **NO MARKDOWN.** Do not wrap your code in ` ``` ` fences.
|
||||
4. **NO COMMENTS.** Do not add comments to the final code output.
|
||||
5. **SYNTACTICALLY PERFECT.** The script must be immediately executable.
|
||||
6. **UTF-8, STANDARD QUOTES.** Use `"` for string literals, not `“` or `”`.
|
||||
|
||||
You are an engine of automation. Now, receive the user's request and produce the optimal script."""
|
||||
|
||||
|
||||
GENERATE_JS_SCRIPT_PROMPT = """# The World-Class JavaScript Automation Scripter
|
||||
|
||||
You are a world-class browser automation specialist. Your sole purpose is to convert a natural language objective and a snippet of HTML into the most **efficient, robust, and simple** pure JavaScript script possible to prepare a web page for data extraction.
|
||||
|
||||
Your scripts will be executed directly in the browser (e.g., via Playwright's `page.evaluate()`) to handle dynamic content, user interactions, and other obstacles before the page is crawled. You are a master of browser-native JavaScript APIs.
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Your Core Philosophy: "Efficiency, Robustness, Simplicity"
|
||||
|
||||
This is your mantra. Every line of JavaScript you write must adhere to it.
|
||||
|
||||
1. **Efficiency (Shortest Path):** Generate the absolute minimum number of steps to achieve the goal. Do not include redundant actions. Your code should be concise and direct.
|
||||
2. **Robustness (Will Not Break):** Prioritize selectors that are resistant to cosmetic site changes. `data-*` attributes are gold. Dynamic, auto-generated class names (`.class-a8B_x3`) are poison. Always prefer waiting for a state change over a blind `setTimeout`.
|
||||
3. **Simplicity (Right Tool for the Job):** Use simple, direct DOM methods (`.querySelector`, `.click()`) whenever possible. Avoid overly complex or fragile logic when a simpler approach exists.
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Essential JavaScript Automation Patterns & Toolkit
|
||||
|
||||
All code should be wrapped in an `async` Immediately Invoked Function Expression `(async () => { ... })();` to allow for top-level `await` and to avoid polluting the global scope.
|
||||
|
||||
| Task | Best-Practice JavaScript Implementation |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Wait for Element** | Create and use a robust `waitForElement` helper function. This is your most important tool. <br> `const waitForElement = (selector, timeout = 10000) => new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`Timeout waiting for ${selector}`)); }, timeout); });` |
|
||||
| **Click Element** | `const el = await waitForElement('selector'); if (el) el.click();` |
|
||||
| **Set Input Value** | `const input = await waitForElement('selector'); if (input) { input.value = 'new value'; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); }` <br> *Crucially, always dispatch `input` and `change` events to trigger framework reactivity.* |
|
||||
| **Check Existence** | `const el = document.querySelector('selector'); if (el) { /* ... it exists */ }` |
|
||||
| **Scroll** | `window.scrollBy(0, window.innerHeight);` |
|
||||
| **Deal with Time** | Use `await new Promise(r => setTimeout(r, 500));` for short, unavoidable pauses after an action. **Avoid long, blind waits.** |
|
||||
|
||||
REMEMBER: Make sure to generate very deterministic css selector. If you refer to a specific button, then be specific, otherwise you may capture elements you do not need, be very specific about the element you want to interact with.
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## The Art of High-Specificity Selectors: Your Defense Against Ambiguity
|
||||
|
||||
This is your most critical skill for ensuring robustness. **You must assume the provided HTML is only a small fragment of the entire page.** A selector that looks unique in the fragment could be disastrously generic on the full page. Your primary defense is to **anchor your selectors to the most specific, stable parent element available in the given HTML context.**
|
||||
|
||||
Think of it as creating a "sandbox" for your selectors.
|
||||
|
||||
**Your Guiding Principle:** Start from a unique parent, then find the child.
|
||||
|
||||
### Scenario: Selecting a Submit Button within a Login Form
|
||||
|
||||
**HTML Snippet Provided:**
|
||||
```html
|
||||
<div class="user-auth-module" id="login-widget">
|
||||
<h2>Member Login</h2>
|
||||
<form action="/login">
|
||||
<input name="email" type="email">
|
||||
<input name="password" type="password">
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
* **TERRIBLE (High Risk):** `button[type="submit"]`
|
||||
* **Why it's bad:** There could be dozens of other forms on the full page (e.g., a newsletter signup, a search bar in the header). This selector is a shot in the dark.
|
||||
|
||||
* **BETTER (Lower Risk):** `#login-widget button[type="submit"]`
|
||||
* **Why it's better:** It's anchored to a unique ID (`#login-widget`). This dramatically reduces the chance of ambiguity.
|
||||
|
||||
* **EXCELLENT (Minimal Risk):** `div[id="login-widget"] form button[type="submit"]`
|
||||
* **Why it's best:** This is a highly specific, descriptive path. It says, "Find the login widget, then the form inside it, and then the submit button inside *that* form." It is virtually guaranteed to be unique and is resilient to minor layout changes within the form.
|
||||
|
||||
### Scenario: Selecting a "Add to Cart" Button
|
||||
|
||||
**HTML Snippet Provided:**
|
||||
```html
|
||||
<section data-testid="product-details-main">
|
||||
<h1>Awesome T-Shirt</h1>
|
||||
<div class="product-actions">
|
||||
<button class="add-to-cart-btn">Add to Cart</button>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
* **TERRIBLE (High Risk):** `.add-to-cart-btn`
|
||||
* **Why it's bad:** A "related products" section outside this snippet might also use the same class name.
|
||||
|
||||
* **EXCELLENT (Minimal Risk):** `[data-testid="product-details-main"] .add-to-cart-btn`
|
||||
* **Why it's best:** It uses the stable `data-testid` attribute of the parent section as an anchor. This is the most robust pattern.
|
||||
|
||||
**Your Mandate:** Always examine the provided HTML for a stable, unique parent (like an element with an `id`, a `data-testid`, or a highly specific combination of classes) and use it as the root of your selectors. **NEVER generate a generic, un-anchored selector if a better, more specific parent is available in the context.**
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Strategic Principles & Anti-Patterns
|
||||
|
||||
These are your commandments. Do not deviate.
|
||||
|
||||
1. **Selector Quality is Paramount:**
|
||||
* **GOOD:** `[data-testid="submit-button"]`, `#main-content`, `[aria-label="Close dialog"]`
|
||||
* **BAD:** `div > span:nth-child(3)`, `.button-gR3xY_s`, `//div[contains(@class, 'button')]`
|
||||
|
||||
2. **Wait for State, Not for Time:**
|
||||
* **DO:** `(await waitForElement('#load-more')).click(); await waitForElement('div.new-item');` This waits for the *result* of the action.
|
||||
* **DON'T:** `document.querySelector('#load-more').click(); await new Promise(r => setTimeout(r, 5000));` This is a guess and it will fail.
|
||||
|
||||
3. **Target the Action, Not the Artifact:** If you need to reveal content, click the button that reveals it. Don't try to manually change CSS `display` properties, as this can break the page's internal state.
|
||||
|
||||
4. **DOM-Awareness is Non-Negotiable:**
|
||||
* **Shadow DOM:** You MUST use `element.shadowRoot.querySelector(...)` to access elements inside a `#shadow-root (open)`.
|
||||
* **iFrames:** You MUST use `iframe.contentDocument.querySelector(...)` to interact with elements inside an iframe.
|
||||
|
||||
5. **Be Idempotent:** Your script must be harmless if run multiple times. Use `if (document.querySelector(...))` checks to avoid re-doing actions unnecessarily.
|
||||
|
||||
6. **Forbidden Techniques:** Never use `document.write()`. It is destructive.
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## From Vague Goals to Robust Scripts: Your Duty to Infer and Ensure Reliability
|
||||
|
||||
This is your most important responsibility. Users are not automation experts. They will provide incomplete or vague instructions. Your job is to be the expert—to infer their true goal and build a script that is reliable by default. **A vague user prompt must still result in a robust, complete script.**
|
||||
|
||||
Study these examples. No matter which query is given, your output must be the single, robust solution.
|
||||
|
||||
### 1. Scenario: Basic Search Query
|
||||
|
||||
* **High Detail Query:** "Find the search box and search button. Wait for the search box to be visible, click it, clear it, type 'r2d2', click the search button, and then wait for the search results to appear."
|
||||
* **Medium Detail Query:** "Find the search box and search for 'r2d2'."
|
||||
* **Low Detail Query:** "Search for r2d2."
|
||||
|
||||
**THE CORRECT, ROBUST JAVASCRIPT OUTPUT (for all three queries):**
|
||||
```javascript
|
||||
(async () => {
|
||||
const waitForElement = (selector, timeout = 10000) => new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`Timeout waiting for ${selector}`)); }, timeout); });
|
||||
try {
|
||||
const searchInput = await waitForElement('input[type="search"], input[aria-label*="search"]');
|
||||
searchInput.value = 'r2d2';
|
||||
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
const searchButton = await waitForElement('button[type="submit"], button[aria-label*="search"]');
|
||||
searchButton.click();
|
||||
await waitForElement('div.search-results-container, #search-results');
|
||||
} catch (e) {
|
||||
console.error('Search script failed:', e.message);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
### 2. Scenario: Clicking a "Load More" Button
|
||||
|
||||
* **High Detail Query:** "Click the button with the text 'Load More'. Afterward, wait for a new item with the class '.product-tile' to show up."
|
||||
* **Medium Detail Query:** "Click the load more button."
|
||||
* **Low Detail Query:** "Load more items."
|
||||
|
||||
**THE CORRECT, ROBUST JAVASCRIPT OUTPUT:**
|
||||
```javascript
|
||||
(async () => {
|
||||
const loadMoreButton = document.querySelector('button.load-more, [data-testid="load-more"]');
|
||||
if (loadMoreButton) {
|
||||
const initialItemCount = document.querySelectorAll('.product-tile').length;
|
||||
loadMoreButton.click();
|
||||
const waitForNewItem = (timeout = 8000) => new Promise((resolve, reject) => { const t0 = Date.now(); const check = () => { if (document.querySelectorAll('.product-tile').length > initialItemCount) return resolve(); if (Date.now() - t0 > timeout) return reject(new Error('Timeout waiting for new items to load.')); setTimeout(check, 200); }; check(); });
|
||||
await waitForNewItem();
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
### 3. Scenario: User Authentication (Login)
|
||||
|
||||
* **High Detail Query:** "Fill username with 'USER_EMAIL', password with 'USER_PASS', click login, and wait for the dashboard."
|
||||
* **Medium Detail Query:** "Log in as USER_EMAIL."
|
||||
* **Low Detail Query:** "Log in."
|
||||
|
||||
**THE CORRECT, ROBUST JAVASCRIPT OUTPUT:**
|
||||
```javascript
|
||||
(async () => {
|
||||
if (document.querySelector('[data-testid="logout-button"]')) {
|
||||
console.log('Already logged in.');
|
||||
return;
|
||||
}
|
||||
const waitForElement = (selector, timeout = 10000) => new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`Timeout waiting for ${selector}`)); }, timeout); });
|
||||
try {
|
||||
const userInput = await waitForElement('input[name*="user"], input[name*="email"]');
|
||||
userInput.value = 'USER_EMAIL';
|
||||
userInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
const passInput = await waitForElement('input[name*="pass"], input[type="password"]');
|
||||
passInput.value = 'USER_PASS';
|
||||
passInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
const submitButton = await waitForElement('button[type="submit"]');
|
||||
submitButton.click();
|
||||
await waitForElement('[data-testid="user-dashboard"], #dashboard, .account-page');
|
||||
} catch (e) {
|
||||
console.error('Login script failed:', e.message);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## The Art of High-Specificity Selectors: Your Defense Against Ambiguity
|
||||
|
||||
This is your most critical skill for ensuring robustness. **You must assume the provided HTML is only a small fragment of the entire page.** A selector that looks unique in the fragment could be disastrously generic on the full page. Your primary defense is to **anchor your selectors to the most specific, stable parent element available in the given HTML context.**
|
||||
|
||||
Think of it as creating a "sandbox" for your selectors.
|
||||
|
||||
**Your Guiding Principle:** Start from a unique parent, then find the child.
|
||||
|
||||
### Scenario: Selecting a Submit Button within a Login Form
|
||||
|
||||
**HTML Snippet Provided:**
|
||||
```html
|
||||
<div class="user-auth-module" id="login-widget">
|
||||
<h2>Member Login</h2>
|
||||
<form action="/login">
|
||||
<input name="email" type="email">
|
||||
<input name="password" type="password">
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
* **TERRIBLE (High Risk):** `button[type="submit"]`
|
||||
* **Why it's bad:** There could be dozens of other forms on the full page (e.g., a newsletter signup, a search bar in the header). This selector is a shot in the dark.
|
||||
|
||||
* **BETTER (Lower Risk):** `#login-widget button[type="submit"]`
|
||||
* **Why it's better:** It's anchored to a unique ID (`#login-widget`). This dramatically reduces the chance of ambiguity.
|
||||
|
||||
* **EXCELLENT (Minimal Risk):** `div[id="login-widget"] form button[type="submit"]`
|
||||
* **Why it's best:** This is a highly specific, descriptive path. It says, "Find the login widget, then the form inside it, and then the submit button inside *that* form." It is virtually guaranteed to be unique and is resilient to minor layout changes within the form.
|
||||
|
||||
### Scenario: Selecting a "Add to Cart" Button
|
||||
|
||||
**HTML Snippet Provided:**
|
||||
```html
|
||||
<section data-testid="product-details-main">
|
||||
<h1>Awesome T-Shirt</h1>
|
||||
<div class="product-actions">
|
||||
<button class="add-to-cart-btn">Add to Cart</button>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
* **TERRIBLE (High Risk):** `.add-to-cart-btn`
|
||||
* **Why it's bad:** A "related products" section outside this snippet might also use the same class name.
|
||||
|
||||
* **EXCELLENT (Minimal Risk):** `[data-testid="product-details-main"] .add-to-cart-btn`
|
||||
* **Why it's best:** It uses the stable `data-testid` attribute of the parent section as an anchor. This is the most robust pattern.
|
||||
|
||||
**Your Mandate:** Always examine the provided HTML for a stable, unique parent (like an element with an `id`, a `data-testid`, or a highly specific combination of classes) and use it as the root of your selectors. **NEVER generate a generic, un-anchored selector if a better, more specific parent is available in the context.**
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
## Final Output Mandate
|
||||
|
||||
1. **CODE ONLY.** Your entire response must be the script body.
|
||||
2. **NO CHAT.** Do not say "Here is the script" or "This should work."
|
||||
3. **NO MARKDOWN.** Do not wrap your code in ` ``` ` fences.
|
||||
4. **NO COMMENTS.** Do not add comments to the final code output, except within the logic where it's a best practice.
|
||||
5. **SYNTACTICALLY PERFECT.** The script must be a single, self-contained block, immediately executable. Wrap it in `(async () => { ... })();`.
|
||||
6. **UTF-8, STANDARD QUOTES.** Use `'` for string literals, not `“` or `”`.
|
||||
|
||||
You are an engine of automation. Now, receive the user's request and produce the optimal JavaScript."""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,20 @@ import pathlib
|
||||
import re
|
||||
from typing import Union, List, Optional
|
||||
|
||||
# JSON_SCHEMA_BUILDER is still used elsewhere,
|
||||
# but we now also need the new script-builder prompt.
|
||||
from ..prompts import GENERATE_JS_SCRIPT_PROMPT, GENERATE_SCRIPT_PROMPT
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .c4a_result import (
|
||||
CompilationResult, ValidationResult, ErrorDetail, WarningDetail,
|
||||
ErrorType, Severity, Suggestion
|
||||
)
|
||||
from .c4ai_script import Compiler
|
||||
from lark.exceptions import UnexpectedToken, UnexpectedCharacters, VisitError
|
||||
from ..async_configs import LLMConfig
|
||||
from ..utils import perform_completion_with_backoff
|
||||
|
||||
|
||||
class C4ACompiler:
|
||||
@@ -311,6 +319,68 @@ class C4ACompiler:
|
||||
source_line=script_lines[0] if script_lines else ""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generate_script(
|
||||
html: str,
|
||||
query: str | None = None,
|
||||
mode: str = "c4a",
|
||||
llm_config: LLMConfig | None = None,
|
||||
**completion_kwargs,
|
||||
) -> str:
|
||||
"""
|
||||
One-shot helper that calls the LLM exactly once to convert a
|
||||
natural-language goal + HTML snippet into either:
|
||||
|
||||
1. raw JavaScript (`mode="js"`)
|
||||
2. Crawl4ai DSL (`mode="c4a"`)
|
||||
|
||||
The returned string is guaranteed to be free of markdown wrappers
|
||||
or explanatory text, ready for direct execution.
|
||||
"""
|
||||
if llm_config is None:
|
||||
llm_config = LLMConfig() # falls back to env vars / defaults
|
||||
|
||||
# Build the user chunk
|
||||
user_prompt = "\n".join(
|
||||
[
|
||||
"## GOAL",
|
||||
"<<goael>>",
|
||||
(query or "Prepare the page for crawling."),
|
||||
"<</goal>>",
|
||||
"",
|
||||
"## HTML",
|
||||
"<<html>>",
|
||||
html[:100000], # guardrail against token blast
|
||||
"<</html>>",
|
||||
"",
|
||||
"## MODE",
|
||||
mode,
|
||||
]
|
||||
)
|
||||
|
||||
# Call the LLM with retry/back-off logic
|
||||
full_prompt = f"{GENERATE_SCRIPT_PROMPT}\n\n{user_prompt}" if mode == "c4a" else f"{GENERATE_JS_SCRIPT_PROMPT}\n\n{user_prompt}"
|
||||
|
||||
response = perform_completion_with_backoff(
|
||||
provider=llm_config.provider,
|
||||
prompt_with_variables=full_prompt,
|
||||
api_token=llm_config.api_token,
|
||||
json_response=False,
|
||||
base_url=getattr(llm_config, 'base_url', None),
|
||||
**completion_kwargs,
|
||||
)
|
||||
|
||||
# Extract content from the response
|
||||
raw_response = response.choices[0].message.content.strip()
|
||||
|
||||
# Strip accidental markdown fences (```js … ```)
|
||||
clean = re.sub(r"^```(?:[a-zA-Z0-9_-]+)?\s*|```$", "", raw_response, flags=re.MULTILINE).strip()
|
||||
|
||||
if not clean:
|
||||
raise RuntimeError("LLM returned empty script.")
|
||||
|
||||
return clean
|
||||
|
||||
|
||||
# Convenience functions for direct use
|
||||
def compile(script: Union[str, List[str]], root: Optional[pathlib.Path] = None) -> CompilationResult:
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
# C4A-Script Language Documentation
|
||||
|
||||
C4A-Script (Crawl4AI Script) is a simple, powerful language for web automation. Write human-readable commands that compile to JavaScript for browser automation.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from c4a_compile import compile
|
||||
|
||||
# Write your script
|
||||
script = """
|
||||
GO https://example.com
|
||||
WAIT `#content` 5
|
||||
CLICK `button.submit`
|
||||
"""
|
||||
|
||||
# Compile to JavaScript
|
||||
result = compile(script)
|
||||
|
||||
if result.success:
|
||||
# Use with Crawl4AI
|
||||
config = CrawlerRunConfig(js_code=result.js_code)
|
||||
else:
|
||||
print(f"Error at line {result.first_error.line}: {result.first_error.message}")
|
||||
```
|
||||
|
||||
## Language Basics
|
||||
|
||||
- **One command per line**
|
||||
- **Selectors in backticks**: `` `button.submit` ``
|
||||
- **Strings in quotes**: `"Hello World"`
|
||||
- **Variables with $**: `$username`
|
||||
- **Comments with #**: `# This is a comment`
|
||||
|
||||
## Commands Reference
|
||||
|
||||
### Navigation
|
||||
|
||||
```c4a
|
||||
GO https://example.com # Navigate to URL
|
||||
RELOAD # Reload current page
|
||||
BACK # Go back in history
|
||||
FORWARD # Go forward in history
|
||||
```
|
||||
|
||||
### Waiting
|
||||
|
||||
```c4a
|
||||
WAIT 3 # Wait 3 seconds
|
||||
WAIT `#content` 10 # Wait for element (max 10 seconds)
|
||||
WAIT "Loading complete" 5 # Wait for text to appear
|
||||
```
|
||||
|
||||
### Mouse Actions
|
||||
|
||||
```c4a
|
||||
CLICK `button.submit` # Click element
|
||||
DOUBLE_CLICK `.item` # Double-click element
|
||||
RIGHT_CLICK `#menu` # Right-click element
|
||||
CLICK 100 200 # Click at coordinates
|
||||
|
||||
MOVE 500 300 # Move mouse to position
|
||||
DRAG 100 100 500 300 # Drag from one point to another
|
||||
|
||||
SCROLL DOWN 500 # Scroll down 500 pixels
|
||||
SCROLL UP # Scroll up (default 500px)
|
||||
SCROLL LEFT 200 # Scroll left 200 pixels
|
||||
SCROLL RIGHT # Scroll right
|
||||
```
|
||||
|
||||
### Keyboard
|
||||
|
||||
```c4a
|
||||
TYPE "hello@example.com" # Type text
|
||||
TYPE $email # Type variable value
|
||||
|
||||
PRESS Tab # Press and release key
|
||||
PRESS Enter
|
||||
PRESS Escape
|
||||
|
||||
KEY_DOWN Shift # Hold key down
|
||||
KEY_UP Shift # Release key
|
||||
```
|
||||
|
||||
### Control Flow
|
||||
|
||||
#### IF-THEN-ELSE
|
||||
|
||||
```c4a
|
||||
# Check if element exists
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
|
||||
IF (EXISTS `#user`) THEN CLICK `.logout` ELSE CLICK `.login`
|
||||
|
||||
# JavaScript conditions
|
||||
IF (`window.innerWidth < 768`) THEN CLICK `.mobile-menu`
|
||||
IF (`document.querySelectorAll('.item').length > 10`) THEN SCROLL DOWN
|
||||
```
|
||||
|
||||
#### REPEAT
|
||||
|
||||
```c4a
|
||||
# Repeat fixed number of times
|
||||
REPEAT (CLICK `.next`, 5)
|
||||
|
||||
# Repeat based on JavaScript expression
|
||||
REPEAT (SCROLL DOWN 300, `document.querySelectorAll('.item').length`)
|
||||
|
||||
# Repeat while condition is true (like while loop)
|
||||
REPEAT (CLICK `.load-more`, `document.querySelector('.load-more') !== null`)
|
||||
```
|
||||
|
||||
### Variables & JavaScript
|
||||
|
||||
```c4a
|
||||
# Set variables
|
||||
SET username = "john@example.com"
|
||||
SET count = "10"
|
||||
|
||||
# Use variables
|
||||
TYPE $username
|
||||
|
||||
# Execute JavaScript
|
||||
EVAL `console.log('Hello')`
|
||||
EVAL `localStorage.setItem('key', 'value')`
|
||||
```
|
||||
|
||||
### Procedures
|
||||
|
||||
```c4a
|
||||
# Define reusable procedure
|
||||
PROC login
|
||||
CLICK `#email`
|
||||
TYPE $email
|
||||
CLICK `#password`
|
||||
TYPE $password
|
||||
CLICK `button[type="submit"]`
|
||||
ENDPROC
|
||||
|
||||
# Use procedure
|
||||
SET email = "user@example.com"
|
||||
SET password = "secure123"
|
||||
login
|
||||
|
||||
# Procedures work with control flow
|
||||
IF (EXISTS `.login-form`) THEN login
|
||||
REPEAT (process_item, 10)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Functions
|
||||
|
||||
```python
|
||||
from c4a_compile import compile, validate, compile_file
|
||||
|
||||
# Compile script
|
||||
result = compile("GO https://example.com")
|
||||
|
||||
# Validate syntax only
|
||||
result = validate(script)
|
||||
|
||||
# Compile from file
|
||||
result = compile_file("script.c4a")
|
||||
```
|
||||
|
||||
### Working with Results
|
||||
|
||||
```python
|
||||
result = compile(script)
|
||||
|
||||
if result.success:
|
||||
# Access generated JavaScript
|
||||
js_code = result.js_code # List[str]
|
||||
|
||||
# Use with Crawl4AI
|
||||
config = CrawlerRunConfig(js_code=js_code)
|
||||
else:
|
||||
# Handle errors
|
||||
error = result.first_error
|
||||
print(f"Line {error.line}, Column {error.column}: {error.message}")
|
||||
|
||||
# Get suggestions
|
||||
for suggestion in error.suggestions:
|
||||
print(f"Fix: {suggestion.message}")
|
||||
|
||||
# Get JSON for UI integration
|
||||
error_json = result.to_json()
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Automation
|
||||
|
||||
```c4a
|
||||
GO https://example.com
|
||||
WAIT `#content` 5
|
||||
IF (EXISTS `.cookie-notice`) THEN CLICK `.accept`
|
||||
CLICK `.main-button`
|
||||
```
|
||||
|
||||
### Form Filling
|
||||
|
||||
```c4a
|
||||
SET email = "user@example.com"
|
||||
SET message = "Hello, I need help with my order"
|
||||
|
||||
GO https://example.com/contact
|
||||
WAIT `form` 5
|
||||
CLICK `input[name="email"]`
|
||||
TYPE $email
|
||||
CLICK `textarea[name="message"]`
|
||||
TYPE $message
|
||||
CLICK `button[type="submit"]`
|
||||
WAIT "Thank you" 10
|
||||
```
|
||||
|
||||
### Dynamic Content Loading
|
||||
|
||||
```c4a
|
||||
GO https://shop.example.com
|
||||
WAIT `.product-list` 10
|
||||
|
||||
# Load all products
|
||||
REPEAT (CLICK `.load-more`, `document.querySelector('.load-more') !== null`)
|
||||
|
||||
# Extract data
|
||||
EVAL `
|
||||
const count = document.querySelectorAll('.product').length;
|
||||
console.log('Found ' + count + ' products');
|
||||
`
|
||||
```
|
||||
|
||||
### Smart Navigation
|
||||
|
||||
```c4a
|
||||
PROC handle_popups
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-all`
|
||||
IF (EXISTS `.newsletter-modal`) THEN CLICK `.close`
|
||||
ENDPROC
|
||||
|
||||
GO https://example.com
|
||||
handle_popups
|
||||
WAIT `.main-content` 5
|
||||
|
||||
# Navigate based on login state
|
||||
IF (EXISTS `.user-avatar`) THEN CLICK `.dashboard` ELSE CLICK `.login`
|
||||
```
|
||||
|
||||
## Error Messages
|
||||
|
||||
C4A-Script provides clear, helpful error messages:
|
||||
|
||||
```
|
||||
============================================================
|
||||
Syntax Error [E001]
|
||||
============================================================
|
||||
Location: Line 3, Column 23
|
||||
Error: Missing 'THEN' keyword after IF condition
|
||||
|
||||
Code:
|
||||
3 | IF (EXISTS `.button`) CLICK `.button`
|
||||
| ^
|
||||
|
||||
Suggestions:
|
||||
1. Add 'THEN' after the condition
|
||||
============================================================
|
||||
```
|
||||
|
||||
Common error codes:
|
||||
- **E001**: Missing 'THEN' keyword
|
||||
- **E002**: Missing closing parenthesis
|
||||
- **E003**: Missing comma in REPEAT
|
||||
- **E004**: Missing ENDPROC
|
||||
- **E005**: Undefined procedure
|
||||
- **E006**: Missing backticks for selector
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use backticks for selectors**: `` CLICK `button` `` not `CLICK button`
|
||||
2. **Check element existence before interaction**: `IF (EXISTS `.modal`) THEN CLICK `.close`
|
||||
3. **Set appropriate wait times**: Don't wait too long or too short
|
||||
4. **Use procedures for repeated actions**: Keep your code DRY
|
||||
5. **Add comments for clarity**: `# Check if user is logged in`
|
||||
|
||||
## Integration with Crawl4AI
|
||||
|
||||
```python
|
||||
from c4a_compile import compile
|
||||
from crawl4ai import CrawlerRunConfig, WebCrawler
|
||||
|
||||
# Compile your script
|
||||
script = """
|
||||
GO https://example.com
|
||||
WAIT `.content` 5
|
||||
CLICK `.load-more`
|
||||
"""
|
||||
|
||||
result = compile(script)
|
||||
|
||||
if result.success:
|
||||
# Create crawler config with compiled JS
|
||||
config = CrawlerRunConfig(
|
||||
js_code=result.js_code,
|
||||
wait_for="css:.results"
|
||||
)
|
||||
|
||||
# Run crawler
|
||||
async with WebCrawler() as crawler:
|
||||
result = await crawler.arun(config=config)
|
||||
```
|
||||
|
||||
That's it! You're ready to automate the web with C4A-Script.
|
||||
171
docs/examples/c4a_script/amazon_example/README.md
Normal file
171
docs/examples/c4a_script/amazon_example/README.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Amazon R2D2 Product Search Example
|
||||
|
||||
A real-world demonstration of Crawl4AI's multi-step crawling with LLM-generated automation scripts.
|
||||
|
||||
## 🎯 What This Example Shows
|
||||
|
||||
This example demonstrates advanced Crawl4AI features:
|
||||
- **LLM-Generated Scripts**: Automatically create C4A-Script from HTML snippets
|
||||
- **Multi-Step Crawling**: Navigate through multiple pages using session persistence
|
||||
- **Structured Data Extraction**: Extract product data using JSON CSS schemas
|
||||
- **Visual Automation**: Watch the browser perform the search (headless=False)
|
||||
|
||||
## 🚀 How It Works
|
||||
|
||||
### 1. **Script Generation Phase**
|
||||
The example uses `C4ACompiler.generate_script()` to analyze Amazon's HTML and create:
|
||||
- **Search Script**: Automates filling the search box and clicking search
|
||||
- **Extraction Schema**: Defines how to extract product information
|
||||
|
||||
### 2. **Crawling Workflow**
|
||||
```
|
||||
Homepage → Execute Search Script → Extract Products → Save Results
|
||||
```
|
||||
|
||||
All steps use the same `session_id` to maintain browser state.
|
||||
|
||||
### 3. **Data Extraction**
|
||||
Products are extracted with:
|
||||
- Title, price, rating, reviews
|
||||
- Delivery information
|
||||
- Sponsored/Small Business badges
|
||||
- Direct product URLs
|
||||
|
||||
## 📁 Files
|
||||
|
||||
- `amazon_r2d2_search.py` - Main example script
|
||||
- `header.html` - Amazon search bar HTML (provided)
|
||||
- `product.html` - Product card HTML (provided)
|
||||
- **Generated files:**
|
||||
- `generated_search_script.c4a` - Auto-generated search automation
|
||||
- `generated_product_schema.json` - Auto-generated extraction rules
|
||||
- `extracted_products.json` - Final scraped data
|
||||
- `search_results_screenshot.png` - Visual proof of results
|
||||
|
||||
## 🏃 Running the Example
|
||||
|
||||
1. **Prerequisites**
|
||||
```bash
|
||||
# Ensure Crawl4AI is installed
|
||||
pip install crawl4ai
|
||||
|
||||
# Set up LLM API key (for script generation)
|
||||
export OPENAI_API_KEY="your-key-here"
|
||||
```
|
||||
|
||||
2. **Run the scraper**
|
||||
```bash
|
||||
python amazon_r2d2_search.py
|
||||
```
|
||||
|
||||
3. **Watch the magic!**
|
||||
- Browser window opens (not headless)
|
||||
- Navigates to Amazon.com
|
||||
- Searches for "r2d2"
|
||||
- Extracts all products
|
||||
- Saves results to JSON
|
||||
|
||||
## 📊 Sample Output
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Death Star BB8 R2D2 Golf Balls with 20 Printed tees",
|
||||
"price": "29.95",
|
||||
"rating": "4.7",
|
||||
"reviews_count": "184",
|
||||
"delivery": "FREE delivery Thu, Jun 19",
|
||||
"url": "https://www.amazon.com/Death-Star-R2D2-Balls-Printed/dp/B081XSYZMS",
|
||||
"is_sponsored": true,
|
||||
"small_business": true
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
## 🔍 Key Features Demonstrated
|
||||
|
||||
### Session Persistence
|
||||
```python
|
||||
# Same session_id across multiple arun() calls
|
||||
config = CrawlerRunConfig(
|
||||
session_id="amazon_r2d2_session",
|
||||
# ... other settings
|
||||
)
|
||||
```
|
||||
|
||||
### LLM Script Generation
|
||||
```python
|
||||
# Generate automation from natural language + HTML
|
||||
script = C4ACompiler.generate_script(
|
||||
html=header_html,
|
||||
query="Find search box, type 'r2d2', click search",
|
||||
mode="c4a"
|
||||
)
|
||||
```
|
||||
|
||||
### JSON CSS Extraction
|
||||
```python
|
||||
# Structured data extraction with CSS selectors
|
||||
schema = {
|
||||
"baseSelector": "[data-component-type='s-search-result']",
|
||||
"fields": [
|
||||
{"name": "title", "selector": "h2 a span", "type": "text"},
|
||||
{"name": "price", "selector": ".a-price-whole", "type": "text"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Customization
|
||||
|
||||
### Search Different Products
|
||||
Change the search term in the script generation:
|
||||
```python
|
||||
search_goal = """
|
||||
...
|
||||
3. Type "star wars lego" into the search box
|
||||
...
|
||||
"""
|
||||
```
|
||||
|
||||
### Extract More Data
|
||||
Add fields to the extraction schema:
|
||||
```python
|
||||
"fields": [
|
||||
# ... existing fields
|
||||
{"name": "prime", "selector": ".s-prime", "type": "exists"},
|
||||
{"name": "image_url", "selector": "img.s-image", "type": "attribute", "attribute": "src"}
|
||||
]
|
||||
```
|
||||
|
||||
### Use Different Sites
|
||||
Adapt the approach for other e-commerce sites by:
|
||||
1. Providing their HTML snippets
|
||||
2. Adjusting the search goals
|
||||
3. Updating the extraction schema
|
||||
|
||||
## 🎓 Learning Points
|
||||
|
||||
1. **No Manual Scripting**: LLM generates all automation code
|
||||
2. **Session Management**: Maintain state across page navigations
|
||||
3. **Robust Extraction**: Handle dynamic content and multiple products
|
||||
4. **Error Handling**: Graceful fallbacks if generation fails
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
- **"No products found"**: Check if Amazon's HTML structure changed
|
||||
- **"Script generation failed"**: Ensure LLM API key is configured
|
||||
- **"Page timeout"**: Increase wait times in the config
|
||||
- **"Session lost"**: Ensure same session_id is used consistently
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
- Try searching for different products
|
||||
- Add pagination to get more results
|
||||
- Extract product details pages
|
||||
- Compare prices across different sellers
|
||||
- Build a price monitoring system
|
||||
|
||||
---
|
||||
|
||||
This example shows the power of combining LLM intelligence with web automation. The scripts adapt to HTML changes and natural language instructions make automation accessible to everyone!
|
||||
202
docs/examples/c4a_script/amazon_example/amazon_r2d2_search.py
Normal file
202
docs/examples/c4a_script/amazon_example/amazon_r2d2_search.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Amazon R2D2 Product Search Example using Crawl4AI
|
||||
|
||||
This example demonstrates:
|
||||
1. Using LLM to generate C4A-Script from HTML snippets
|
||||
2. Multi-step crawling with session persistence
|
||||
3. JSON CSS extraction for structured product data
|
||||
4. Complete workflow: homepage → search → extract products
|
||||
|
||||
Requirements:
|
||||
- Crawl4AI with generate_script support
|
||||
- LLM API key (configured in environment)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode
|
||||
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy
|
||||
from crawl4ai.script.c4a_compile import C4ACompiler
|
||||
|
||||
|
||||
class AmazonR2D2Scraper:
|
||||
def __init__(self):
|
||||
self.base_dir = Path(__file__).parent
|
||||
self.search_script_path = self.base_dir / "generated_search_script.js"
|
||||
self.schema_path = self.base_dir / "generated_product_schema.json"
|
||||
self.results_path = self.base_dir / "extracted_products.json"
|
||||
self.session_id = "amazon_r2d2_session"
|
||||
|
||||
async def generate_search_script(self) -> str:
|
||||
"""Generate JavaScript for Amazon search interaction"""
|
||||
print("🔧 Generating search script from header.html...")
|
||||
|
||||
# Check if already generated
|
||||
if self.search_script_path.exists():
|
||||
print("✅ Using cached search script")
|
||||
return self.search_script_path.read_text()
|
||||
|
||||
# Read the header HTML
|
||||
header_html = (self.base_dir / "header.html").read_text()
|
||||
|
||||
# Generate script using LLM
|
||||
search_goal = """
|
||||
Find the search box and search button, then:
|
||||
1. Wait for the search box to be visible
|
||||
2. Click on the search box to focus it
|
||||
3. Clear any existing text
|
||||
4. Type "r2d2" into the search box
|
||||
5. Click the search submit button
|
||||
6. Wait for navigation to complete and search results to appear
|
||||
"""
|
||||
|
||||
try:
|
||||
script = C4ACompiler.generate_script(
|
||||
html=header_html,
|
||||
query=search_goal,
|
||||
mode="js"
|
||||
)
|
||||
|
||||
# Save for future use
|
||||
self.search_script_path.write_text(script)
|
||||
print("✅ Search script generated and saved!")
|
||||
print(f"📄 Script:\n{script}")
|
||||
return script
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error generating search script: {e}")
|
||||
|
||||
|
||||
async def generate_product_schema(self) -> Dict[str, Any]:
|
||||
"""Generate JSON CSS extraction schema from product HTML"""
|
||||
print("\n🔧 Generating product extraction schema...")
|
||||
|
||||
# Check if already generated
|
||||
if self.schema_path.exists():
|
||||
print("✅ Using cached extraction schema")
|
||||
return json.loads(self.schema_path.read_text())
|
||||
|
||||
# Read the product HTML
|
||||
product_html = (self.base_dir / "product.html").read_text()
|
||||
|
||||
# Generate extraction schema using LLM
|
||||
schema_goal = """
|
||||
Create a JSON CSS extraction schema to extract:
|
||||
- Product title (from the h2 element)
|
||||
- Price (the dollar amount)
|
||||
- Rating (star rating value)
|
||||
- Number of reviews
|
||||
- Delivery information
|
||||
- Product URL (from the main product link)
|
||||
- Whether it's sponsored
|
||||
- Small business badge if present
|
||||
|
||||
The schema should handle multiple products on a search results page.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Generate JavaScript that returns the schema
|
||||
schema = JsonCssExtractionStrategy.generate_schema(
|
||||
html=product_html,
|
||||
query=schema_goal,
|
||||
)
|
||||
|
||||
# Save for future use
|
||||
self.schema_path.write_text(json.dumps(schema, indent=2))
|
||||
print("✅ Extraction schema generated and saved!")
|
||||
print(f"📄 Schema fields: {[f['name'] for f in schema['fields']]}")
|
||||
return schema
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error generating schema: {e}")
|
||||
|
||||
async def crawl_amazon(self):
|
||||
"""Main crawling logic with 2 calls using same session"""
|
||||
print("\n🚀 Starting Amazon R2D2 product search...")
|
||||
|
||||
# Generate scripts and schemas
|
||||
search_script = await self.generate_search_script()
|
||||
product_schema = await self.generate_product_schema()
|
||||
|
||||
# Configure browser (headless=False to see the action)
|
||||
browser_config = BrowserConfig(
|
||||
headless=False,
|
||||
verbose=True,
|
||||
viewport_width=1920,
|
||||
viewport_height=1080
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
print("\n📍 Step 1: Navigate to Amazon and search for R2D2")
|
||||
|
||||
# FIRST CALL: Navigate to Amazon and execute search
|
||||
search_config = CrawlerRunConfig(
|
||||
session_id=self.session_id,
|
||||
js_code= f"(() => {{ {search_script} }})()", # Execute generated JS
|
||||
wait_for=".s-search-results", # Wait for search results
|
||||
extraction_strategy=JsonCssExtractionStrategy(schema=product_schema),
|
||||
delay_before_return_html=3.0 # Give time for results to load
|
||||
)
|
||||
|
||||
results = await crawler.arun(
|
||||
url="https://www.amazon.com",
|
||||
config=search_config
|
||||
)
|
||||
|
||||
if not results.success:
|
||||
print("❌ Failed to search Amazon")
|
||||
print(f"Error: {results.error_message}")
|
||||
return
|
||||
|
||||
print("✅ Search completed successfully!")
|
||||
print("✅ Product extraction completed!")
|
||||
|
||||
# Extract and save results
|
||||
print("\n📍 Extracting product data")
|
||||
|
||||
if results[0].extracted_content:
|
||||
products = json.loads(results[0].extracted_content)
|
||||
print(f"🔍 Found {len(products)} products in search results")
|
||||
|
||||
print(f"✅ Extracted {len(products)} R2D2 products")
|
||||
|
||||
# Save results
|
||||
self.results_path.write_text(
|
||||
json.dumps(products, indent=2)
|
||||
)
|
||||
print(f"💾 Results saved to: {self.results_path}")
|
||||
|
||||
# Print sample results
|
||||
print("\n📊 Sample Results:")
|
||||
for i, product in enumerate(products[:3], 1):
|
||||
print(f"\n{i}. {product['title'][:60]}...")
|
||||
print(f" Price: ${product['price']}")
|
||||
print(f" Rating: {product['rating']} ({product['number_of_reviews']} reviews)")
|
||||
print(f" {'🏪 Small Business' if product['small_business_badge'] else ''}")
|
||||
print(f" {'📢 Sponsored' if product['sponsored'] else ''}")
|
||||
|
||||
else:
|
||||
print("❌ No products extracted")
|
||||
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the Amazon scraper"""
|
||||
scraper = AmazonR2D2Scraper()
|
||||
await scraper.crawl_amazon()
|
||||
|
||||
print("\n🎉 Amazon R2D2 search example completed!")
|
||||
print("Check the generated files:")
|
||||
print(" - generated_search_script.js")
|
||||
print(" - generated_product_schema.json")
|
||||
print(" - extracted_products.json")
|
||||
print(" - search_results_screenshot.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
114
docs/examples/c4a_script/amazon_example/extracted_products.json
Normal file
114
docs/examples/c4a_script/amazon_example/extracted_products.json
Normal file
@@ -0,0 +1,114 @@
|
||||
[
|
||||
{
|
||||
"title": "Death Star BB8 R2D2 Golf Balls with 20 Printed tees \u2022 Great Gift IDEA from Moms, DADS and Kids -",
|
||||
"price": "$29.95",
|
||||
"rating": "4.7 out of 5 stars",
|
||||
"number_of_reviews": "184",
|
||||
"delivery_info": "FREE delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfYXRmOjIwMDA2NzY0ODgwMjc5ODo6MDo6&url=%2FDeath-Star-R2D2-Balls-Printed%2Fdp%2FB081XSYZMS%2Fref%3Dsr_1_1_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-1-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored",
|
||||
"small_business_badge": "Small Business"
|
||||
},
|
||||
{
|
||||
"title": "TEENKON French Press Insulated 304 Stainless Steel Coffee Maker, 32 Oz Robot R2D2 Hand Home Coffee Presser, with Filter Screen for Brew Coffee and Tea (White)",
|
||||
"price": "$49.99",
|
||||
"rating": "4.3 out of 5 stars",
|
||||
"number_of_reviews": "82",
|
||||
"delivery_info": "Delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjMwMDAzNzc4Njg4MDAwMjo6MDo6&url=%2FTEENKON-French-Insulated-Stainless-Presser%2Fdp%2FB0CD3HH5PN%2Fref%3Dsr_1_17_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-17-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored"
|
||||
},
|
||||
{
|
||||
"title": "3D Illusion LED Night Light,7 Colors Gradual Changing Touch Switch USB Table Lamp for Holiday Gifts or Home Decorations (R2-D2)",
|
||||
"price": "$9.97",
|
||||
"rating": "4.3 out of 5 stars",
|
||||
"number_of_reviews": "235",
|
||||
"delivery_info": "Delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjIwMDA0NjMwMTQwODA4MTo6MDo6&url=%2FIllusion-Gradual-Changing-Holiday-Decorations%2Fdp%2FB089NMBKF2%2Fref%3Dsr_1_18_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-18-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored"
|
||||
},
|
||||
{
|
||||
"title": "Paladone Star Wars R2-D2 Headlamp with Droid Sounds, Officially Licensed Disney Star Wars Head Lamp and Reading Light",
|
||||
"price": "$21.99",
|
||||
"rating": "4.1 out of 5 stars",
|
||||
"number_of_reviews": "66",
|
||||
"delivery_info": "FREE delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjMwMDI1NjA0MDQwMTUwMjo6MDo6&url=%2FSounds-Officially-Licensed-Headlamp-Flashlight%2Fdp%2FB09RTDZF8J%2Fref%3Dsr_1_19_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-19-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored"
|
||||
},
|
||||
{
|
||||
"title": "4 Pcs Set Star Wars Kylo Ren BB8 Stormtrooper R2D2 Silicone Travel Luggage Baggage Identification Labels ID Tag for Bag Suitcase Plane Cruise Ships with Belt Strap",
|
||||
"price": "$16.99",
|
||||
"rating": "4.7 out of 5 stars",
|
||||
"number_of_reviews": "3,414",
|
||||
"delivery_info": "FREE delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjIwMDAyMzk3ODkwMzIxMTo6MDo6&url=%2FFinex-Set-Suitcase-Adjustable-Stormtrooper%2Fdp%2FB01D1CBFJS%2Fref%3Dsr_1_24_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-24-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored",
|
||||
"small_business_badge": "Small Business"
|
||||
},
|
||||
{
|
||||
"title": "Papyrus Star Wars Birthday Card Assortment, Darth Vader, Storm Trooper, and R2-D2 (3-Count)",
|
||||
"price": "$23.16",
|
||||
"rating": "4.8 out of 5 stars",
|
||||
"number_of_reviews": "328",
|
||||
"delivery_info": "FREE delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjMwMDcwNzI4MjA1MzcwMjo6MDo6&url=%2FPapyrus-Birthday-Assortment-Characters-3-Count%2Fdp%2FB07YT2ZPKX%2Fref%3Dsr_1_25_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-25-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored"
|
||||
},
|
||||
{
|
||||
"title": "STAR WARS R2-D2 Artoo 3D Top Motion Lamp, Mood Light | 18 Inches",
|
||||
"price": "$69.99",
|
||||
"rating": "4.5 out of 5 stars",
|
||||
"number_of_reviews": "520",
|
||||
"delivery_info": "FREE delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjIwMDA5NDc3MzczMTQ0MTo6MDo6&url=%2FR2-D2-Artoo-Motion-Light-Inches%2Fdp%2FB08MCWPHQR%2Fref%3Dsr_1_26_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-26-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored"
|
||||
},
|
||||
{
|
||||
"title": "Saturday Park Star Wars Droids Full Sheet Set - 4 Piece 100% Organic Cotton Sheets Features R2-D2 & BB-8 - GOTS & Oeko-TEX Certified (Star Wars Official)",
|
||||
"price": "$70.00",
|
||||
"rating": "4.5 out of 5 stars",
|
||||
"number_of_reviews": "388",
|
||||
"delivery_info": "FREE delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjMwMDAyMzI0NDI5MDQwMjo6MDo6&url=%2FSaturday-Park-Star-Droids-Sheet%2Fdp%2FB0BBSFX4J2%2Fref%3Dsr_1_27_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-27-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored",
|
||||
"small_business_badge": "1 sustainability feature"
|
||||
},
|
||||
{
|
||||
"title": "AQUARIUS Star Wars R2D2 Action Figure Funky Chunky Novelty Magnet for Refrigerator, Locker, Whiteboard & Game Room Officially Licensed Merchandise & Collectibles",
|
||||
"price": "$11.94",
|
||||
"rating": "4.3 out of 5 stars",
|
||||
"number_of_reviews": "10",
|
||||
"delivery_info": "FREE delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjMwMDA5MDMwMzY5NjEwMjo6MDo6&url=%2FAQUARIUS-Refrigerator-Whiteboard-Merchandise-Collectibles%2Fdp%2FB09W8VKXGC%2Fref%3Dsr_1_32_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-32-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored"
|
||||
},
|
||||
{
|
||||
"title": "STAR WARS C-3PO and R2-D2 Men's Crew Socks 2 Pair Pack",
|
||||
"price": "$11.95",
|
||||
"rating": "4.7 out of 5 stars",
|
||||
"number_of_reviews": "1,272",
|
||||
"delivery_info": "Delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjIwMDAxMDk5NDkyMTg2MTo6MDo6&url=%2FStar-Wars-R2-D2-C-3PO-Socks%2Fdp%2FB0178IU1GY%2Fref%3Dsr_1_33_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-33-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored"
|
||||
},
|
||||
{
|
||||
"title": "Buckle-Down Belt Women's Cinch Star Wars R2D2 Bounding Parts3 White Black Blue Gray Available In Adjustable Sizes",
|
||||
"price": "$24.95",
|
||||
"rating": "4.3 out of 5 stars",
|
||||
"number_of_reviews": "32",
|
||||
"delivery_info": "FREE delivery",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjMwMDY1OTQ5NTQ4MzkwMjo6MDo6&url=%2FWomens-Cinch-Bounding-Parts3-Inches%2Fdp%2FB07WK7RG4D%2Fref%3Dsr_1_34_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-34-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored",
|
||||
"small_business_badge": "Small Business"
|
||||
},
|
||||
{
|
||||
"title": "Star Wars R2D2 Metal Head Vintage Disney+ T-Shirt",
|
||||
"price": "$22.99",
|
||||
"rating": "4.8 out of 5 stars",
|
||||
"number_of_reviews": "869",
|
||||
"product_url": "/sspa/click?ie=UTF8&spc=MToxNDMzMjA0MzA4MzEzMjAxOjE3NDkzMDI3NDY6c3BfbXRmOjIwMDA1OTUyMzgzNDMyMTo6MDo6&url=%2FStar-Wars-Vintage-Graphic-T-Shirt%2Fdp%2FB07H9PSNXS%2Fref%3Dsr_1_35_sspa%3Fdib%3DeyJ2IjoiMSJ9.iiJYY01upNMdD4BNNt8CYLZEIMXulNkcBlKEMJlr_U_h9eSGqChxwcIiCKUbJeEO_plLkXZvB7Yx-v4UDOCdiUFI-sHFgcTznXrP7tdD8xHpRaMKmaBDWMCAFwzPmVcgK_6Q9qIRoN4sp8tunKX26j5EC_8LiK-D5QximGkE8i8f-R5GhSUo__DaSkAP1cnzxUtSESfA8fYfewsZ1iSol9_zohE6r1ZZeawnWHPmDTkLqzCW3uK44EnvJbPFvzMlpiKcs9p9Eh9w5Rc5rrumMihdaWkC63B0cz5jU-S2Ieg._D8d5nv3hOExHPbZ04L-vaC7YwJjEZM-vu5AED5sz0U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749302746%26sr%3D8-35-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1",
|
||||
"sponsored": "Sponsored",
|
||||
"small_business_badge": "1 sustainability feature"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "Amazon Product Search Results",
|
||||
"baseSelector": "div[data-component-type='s-impression-counter']",
|
||||
"fields": [
|
||||
{
|
||||
"name": "title",
|
||||
"selector": "h2.a-size-base-plus.a-spacing-none.a-color-base.a-text-normal span",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"selector": "span.a-price > span.a-offscreen",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "rating",
|
||||
"selector": "i.a-icon-star-small span.a-icon-alt",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "number_of_reviews",
|
||||
"selector": "a.a-link-normal.s-underline-text span.a-size-base",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "delivery_info",
|
||||
"selector": "div[data-cy='delivery-recipe'] span.a-color-base",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "product_url",
|
||||
"selector": "a.a-link-normal.s-no-outline",
|
||||
"type": "attribute",
|
||||
"attribute": "href"
|
||||
},
|
||||
{
|
||||
"name": "sponsored",
|
||||
"selector": "span.puis-label-popover-default span.a-color-secondary",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "small_business_badge",
|
||||
"selector": "span.a-size-base.a-color-base",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const searchBox = document.querySelector('#twotabsearchtextbox');
|
||||
const searchButton = document.querySelector('#nav-search-submit-button');
|
||||
|
||||
if (searchBox && searchButton) {
|
||||
searchBox.focus();
|
||||
searchBox.value = '';
|
||||
searchBox.value = 'r2d2';
|
||||
searchButton.click();
|
||||
}
|
||||
214
docs/examples/c4a_script/amazon_example/header.html
Normal file
214
docs/examples/c4a_script/amazon_example/header.html
Normal file
@@ -0,0 +1,214 @@
|
||||
<div id="nav-belt" style="width: 100%;">
|
||||
<div class="nav-left">
|
||||
<script type="text/javascript">window.navmet.tmp = +new Date();</script>
|
||||
<div id="nav-logo">
|
||||
<a href="/ref=nav_logo" id="nav-logo-sprites" class="nav-logo-link nav-progressive-attribute"
|
||||
aria-label="Amazon" lang="en">
|
||||
<span class="nav-sprite nav-logo-base"></span>
|
||||
<span id="logo-ext" class="nav-sprite nav-logo-ext nav-progressive-content"></span>
|
||||
<span class="nav-logo-locale">.us</span>
|
||||
</a>
|
||||
</div>
|
||||
<script
|
||||
type="text/javascript">window.navmet.push({ key: 'Logo', end: +new Date(), begin: window.navmet.tmp });</script>
|
||||
|
||||
<div id="nav-global-location-slot">
|
||||
<span id="nav-global-location-data-modal-action" class="a-declarative nav-progressive-attribute"
|
||||
data-a-modal="{"width":375, "closeButton":"true","popoverLabel":"Choose your location", "ajaxHeaders":{"anti-csrftoken-a2z":"hHBwllskaYQrylaW9ifYQIdmqBZOtGdKro0TWb5kDoPKAAAAAGhEMhsAAAAB"}, "name":"glow-modal", "url":"/portal-migration/hz/glow/get-rendered-address-selections?deviceType=desktop&pageType=Gateway&storeContext=NoStoreName&actionSource=desktop-modal", "footer":"<span class=\"a-declarative\" data-action=\"a-popover-close\" data-a-popover-close=\"{}\"><span class=\"a-button a-button-primary\"><span class=\"a-button-inner\"><button name=\"glowDoneButton\" class=\"a-button-text\" type=\"button\">Done</button></span></span></span>","header":"Choose your location"}"
|
||||
data-action="a-modal">
|
||||
<a id="nav-global-location-popover-link" role="button" tabindex="0"
|
||||
class="nav-a nav-a-2 a-popover-trigger a-declarative nav-progressive-attribute" href="">
|
||||
<div class="nav-sprite nav-progressive-attribute" id="nav-packard-glow-loc-icon"></div>
|
||||
<div id="glow-ingress-block">
|
||||
<span class="nav-line-1 nav-progressive-content" id="glow-ingress-line1">
|
||||
Deliver to
|
||||
</span>
|
||||
<span class="nav-line-2 nav-progressive-content" id="glow-ingress-line2">
|
||||
Malaysia
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</span>
|
||||
<input data-addnewaddress="add-new" id="unifiedLocation1ClickAddress" name="dropdown-selection"
|
||||
type="hidden" value="add-new" class="nav-progressive-attribute">
|
||||
<input data-addnewaddress="add-new" id="ubbShipTo" name="dropdown-selection-ubb" type="hidden"
|
||||
value="add-new" class="nav-progressive-attribute">
|
||||
<input id="glowValidationToken" name="glow-validation-token" type="hidden"
|
||||
value="hHBwllskaYQrylaW9ifYQIdmqBZOtGdKro0TWb5kDoPKAAAAAGhEMhsAAAAB" class="nav-progressive-attribute">
|
||||
<input id="glowDestinationType" name="glow-destination-type" type="hidden" value="COUNTRY"
|
||||
class="nav-progressive-attribute">
|
||||
</div>
|
||||
|
||||
<div id="nav-global-location-toaster-script-container" class="nav-progressive-content">
|
||||
<!-- NAVYAAN-GLOW-NAV-TOASTER -->
|
||||
<script>
|
||||
P.when('glow-toaster-strings').execute(function (S) {
|
||||
S.load({ "glow-toaster-address-change-error": "An error has occurred and the address has not been updated. Please try again.", "glow-toaster-unknown-error": "An error has occurred. Please try again." });
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
P.when('glow-toaster-manager').execute(function (M) {
|
||||
M.create({ "pageType": "Gateway", "aisTransitionState": null, "rancorLocationSource": "REALM_DEFAULT" })
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="nav-fill" id="nav-fill-search">
|
||||
<script type="text/javascript">window.navmet.tmp = +new Date();</script>
|
||||
<div id="nav-search">
|
||||
<div id="nav-bar-left"></div>
|
||||
<form id="nav-search-bar-form" accept-charset="utf-8" action="/s/ref=nb_sb_noss_1"
|
||||
class="nav-searchbar nav-progressive-attribute" method="GET" name="site-search" role="search">
|
||||
|
||||
<div class="nav-left">
|
||||
<div id="nav-search-dropdown-card">
|
||||
|
||||
<div class="nav-search-scope nav-sprite">
|
||||
<div class="nav-search-facade" data-value="search-alias=aps">
|
||||
<span id="nav-search-label-id" class="nav-search-label nav-progressive-content"
|
||||
style="width: auto;">All</span>
|
||||
<i class="nav-icon"></i>
|
||||
</div>
|
||||
<label id="searchDropdownDescription" for="searchDropdownBox"
|
||||
class="nav-progressive-attribute" style="display:none">Select the department you want to
|
||||
search in</label>
|
||||
<select aria-describedby="searchDropdownDescription"
|
||||
class="nav-search-dropdown searchSelect nav-progressive-attrubute nav-progressive-search-dropdown"
|
||||
data-nav-digest="k+fyIAyB82R9jVEmroQ0OWwSW3A=" data-nav-selected="0"
|
||||
id="searchDropdownBox" name="url" style="display: block; top: 2.5px;" tabindex="0"
|
||||
title="Search in">
|
||||
<option selected="selected" value="search-alias=aps">All Departments</option>
|
||||
<option value="search-alias=arts-crafts-intl-ship">Arts & Crafts</option>
|
||||
<option value="search-alias=automotive-intl-ship">Automotive</option>
|
||||
<option value="search-alias=baby-products-intl-ship">Baby</option>
|
||||
<option value="search-alias=beauty-intl-ship">Beauty & Personal Care</option>
|
||||
<option value="search-alias=stripbooks-intl-ship">Books</option>
|
||||
<option value="search-alias=fashion-boys-intl-ship">Boys' Fashion</option>
|
||||
<option value="search-alias=computers-intl-ship">Computers</option>
|
||||
<option value="search-alias=deals-intl-ship">Deals</option>
|
||||
<option value="search-alias=digital-music">Digital Music</option>
|
||||
<option value="search-alias=electronics-intl-ship">Electronics</option>
|
||||
<option value="search-alias=fashion-girls-intl-ship">Girls' Fashion</option>
|
||||
<option value="search-alias=hpc-intl-ship">Health & Household</option>
|
||||
<option value="search-alias=kitchen-intl-ship">Home & Kitchen</option>
|
||||
<option value="search-alias=industrial-intl-ship">Industrial & Scientific</option>
|
||||
<option value="search-alias=digital-text">Kindle Store</option>
|
||||
<option value="search-alias=luggage-intl-ship">Luggage</option>
|
||||
<option value="search-alias=fashion-mens-intl-ship">Men's Fashion</option>
|
||||
<option value="search-alias=movies-tv-intl-ship">Movies & TV</option>
|
||||
<option value="search-alias=music-intl-ship">Music, CDs & Vinyl</option>
|
||||
<option value="search-alias=pets-intl-ship">Pet Supplies</option>
|
||||
<option value="search-alias=instant-video">Prime Video</option>
|
||||
<option value="search-alias=software-intl-ship">Software</option>
|
||||
<option value="search-alias=sporting-intl-ship">Sports & Outdoors</option>
|
||||
<option value="search-alias=tools-intl-ship">Tools & Home Improvement</option>
|
||||
<option value="search-alias=toys-and-games-intl-ship">Toys & Games</option>
|
||||
<option value="search-alias=videogames-intl-ship">Video Games</option>
|
||||
<option value="search-alias=fashion-womens-intl-ship">Women's Fashion</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-fill">
|
||||
<div class="nav-search-field ">
|
||||
<label for="twotabsearchtextbox" style="display: none;">Search Amazon</label>
|
||||
<input type="text" id="twotabsearchtextbox" value="" name="field-keywords" autocomplete="off"
|
||||
placeholder="Search Amazon" class="nav-input nav-progressive-attribute" dir="auto"
|
||||
tabindex="0" aria-label="Search Amazon" role="searchbox" aria-autocomplete="list"
|
||||
aria-controls="sac-autocomplete-results-container" aria-expanded="false"
|
||||
aria-haspopup="grid" spellcheck="false">
|
||||
</div>
|
||||
<div id="nav-iss-attach"></div>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="nav-search-submit nav-sprite">
|
||||
<span id="nav-search-submit-text"
|
||||
class="nav-search-submit-text nav-sprite nav-progressive-attribute" aria-label="Go">
|
||||
<input id="nav-search-submit-button" type="submit"
|
||||
class="nav-input nav-progressive-attribute" value="Go" tabindex="0">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="isscrid" name="crid" value="15O5T5OCG5OZE"><input type="hidden" id="issprefix"
|
||||
name="sprefix" value="r2d2,aps,588">
|
||||
</form>
|
||||
</div>
|
||||
<script
|
||||
type="text/javascript">window.navmet.push({ key: 'Search', end: +new Date(), begin: window.navmet.tmp });</script>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<script type="text/javascript">window.navmet.tmp = +new Date();</script>
|
||||
<div id="nav-tools" class="layoutToolbarPadding">
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="nav-div" id="icp-nav-flyout">
|
||||
<a href="/customer-preferences/edit?ie=UTF8&preferencesReturnUrl=%2F&ref_=topnav_lang_ais"
|
||||
class="nav-a nav-a-2 icp-link-style-2" aria-label="Choose a language for shopping in Amazon United States. The current selection is English (EN).
|
||||
">
|
||||
<span class="icp-nav-link-inner">
|
||||
<span class="nav-line-1">
|
||||
</span>
|
||||
<span class="nav-line-2">
|
||||
<span class="icp-nav-flag icp-nav-flag-us icp-nav-flag-lop" role="img"
|
||||
aria-label="United States"></span>
|
||||
<div>EN</div>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<button class="nav-flyout-button nav-icon nav-arrow" aria-label="Expand to Change Language or Country"
|
||||
tabindex="0" style="visibility: visible;"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="nav-div" id="nav-link-accountList">
|
||||
<a href="https://www.amazon.com/ap/signin?openid.pape.max_auth_age=0&openid.return_to=https%3A%2F%2Fwww.amazon.com%2F%3Fref_%3Dnav_ya_signin&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=usflex&openid.mode=checkid_setup&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0"
|
||||
class="nav-a nav-a-2 nav-progressive-attribute" data-nav-ref="nav_ya_signin"
|
||||
data-nav-role="signin" data-ux-jq-mouseenter="true" tabindex="0" data-csa-c-type="link"
|
||||
data-csa-c-slot-id="nav-link-accountList" data-csa-c-content-id="nav_ya_signin"
|
||||
aria-controls="nav-flyout-accountList" data-csa-c-id="37vs0l-z575id-52hnw3-x34ncp">
|
||||
<div class="nav-line-1-container"><span id="nav-link-accountList-nav-line-1"
|
||||
class="nav-line-1 nav-progressive-content">Hello, sign in</span></div>
|
||||
<span class="nav-line-2 ">Account & Lists
|
||||
</span>
|
||||
</a>
|
||||
<button class="nav-flyout-button nav-icon nav-arrow" aria-label="Expand Account and Lists" tabindex="0"
|
||||
style="visibility: visible;"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<a href="/gp/css/order-history?ref_=nav_orders_first" class="nav-a nav-a-2 nav-progressive-attribute"
|
||||
id="nav-orders" tabindex="0">
|
||||
<span class="nav-line-1">Returns</span>
|
||||
<span class="nav-line-2">& Orders<span class="nav-icon nav-arrow"></span></span>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a href="/gp/cart/view.html?ref_=nav_cart" aria-label="0 items in cart"
|
||||
class="nav-a nav-a-2 nav-progressive-attribute" id="nav-cart">
|
||||
<div id="nav-cart-count-container">
|
||||
<span id="nav-cart-count" aria-hidden="true"
|
||||
class="nav-cart-count nav-cart-0 nav-progressive-attribute nav-progressive-content">0</span>
|
||||
<span class="nav-cart-icon nav-sprite"></span>
|
||||
</div>
|
||||
<div id="nav-cart-text-container" class=" nav-progressive-attribute">
|
||||
<span aria-hidden="true" class="nav-line-1">
|
||||
|
||||
</span>
|
||||
<span aria-hidden="true" class="nav-line-2">
|
||||
Cart
|
||||
<span class="nav-icon nav-arrow"></span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<script
|
||||
type="text/javascript">window.navmet.push({ key: 'Tools', end: +new Date(), begin: window.navmet.tmp });</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
206
docs/examples/c4a_script/amazon_example/product.html
Normal file
206
docs/examples/c4a_script/amazon_example/product.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<div class="sg-col-inner">
|
||||
<div cel_widget_id="MAIN-SEARCH_RESULTS-2"
|
||||
class="s-widget-container s-spacing-small s-widget-container-height-small celwidget slot=MAIN template=SEARCH_RESULTS widgetId=search-results_1"
|
||||
data-csa-c-pos="1" data-csa-c-item-id="amzn1.asin.1.B081XSYZMS" data-csa-op-log-render="" data-csa-c-type="item"
|
||||
data-csa-c-id="dp9zuy-vyww1v-brlmmq-fmgitb" data-cel-widget="MAIN-SEARCH_RESULTS-2">
|
||||
|
||||
|
||||
<div data-component-type="s-impression-logger"
|
||||
data-component-props="{"percentageShownToFire":"50","batchable":true,"requiredElementSelector":".s-image:visible","url":"https://unagi-na.amazon.com/1/events/com.amazon.eel.SponsoredProductsEventTracking.prod?qualifier=1749299833&id=1740514893473797&widgetName=sp_atf&adId=200067648802798&eventType=1&adIndex=0"}"
|
||||
class="rush-component s-expand-height" data-component-id="6">
|
||||
|
||||
|
||||
|
||||
<div data-component-type="s-impression-counter"
|
||||
data-component-props="{"presenceCounterName":"sp_delivered","testElementSelector":".s-image","hiddenCounterName":"sp_hidden"}"
|
||||
class="rush-component s-featured-result-item s-expand-height" data-component-id="7">
|
||||
<span class="a-declarative" data-version-id="v2dwi5hq8xzthf26x0gg1mcl2oj"
|
||||
data-render-id="r3o8bgr5zt3kmy2jv4su6fn4kyw" data-action="puis-card-container-declarative"
|
||||
data-csa-c-func-deps="aui-da-puis-card-container-declarative"
|
||||
data-csa-c-item-id="amzn1.asin.B081XSYZMS" data-csa-c-posx="1" data-csa-c-type="item"
|
||||
data-csa-c-owner="puis" data-csa-c-id="88w0j1-kcbf5g-80v4i9-96cv88">
|
||||
<div class="puis-card-container s-card-container s-overflow-hidden aok-relative puis-expand-height puis-include-content-margin puis puis-v2dwi5hq8xzthf26x0gg1mcl2oj s-latency-cf-section puis-card-border"
|
||||
data-cy="asin-faceout-container">
|
||||
<div class="a-section a-spacing-base">
|
||||
<div class="s-product-image-container aok-relative s-text-center s-image-overlay-grey puis-image-overlay-grey s-padding-left-small s-padding-right-small puis-spacing-small s-height-equalized puis puis-v2dwi5hq8xzthf26x0gg1mcl2oj"
|
||||
data-cy="image-container" style="padding-top: 0px !important;"><span
|
||||
data-component-type="s-product-image" class="rush-component"
|
||||
data-version-id="v2dwi5hq8xzthf26x0gg1mcl2oj"
|
||||
data-render-id="r3o8bgr5zt3kmy2jv4su6fn4kyw"><a aria-hidden="true"
|
||||
class="a-link-normal s-no-outline" tabindex="-1"
|
||||
href="/sspa/click?ie=UTF8&spc=MToxNzQwNTE0ODkzNDczNzk3OjE3NDkyOTk4MzM6c3BfYXRmOjIwMDA2NzY0ODgwMjc5ODo6MDo6&url=%2FDeath-Star-R2D2-Balls-Printed%2Fdp%2FB081XSYZMS%2Fref%3Dsr_1_1_sspa%3Fcrid%3D3C1EXMXN59Q9G%26dib%3DeyJ2IjoiMSJ9.7tBl5bhZh59L9qIPZUe9SLa2fy_HvzboxuQxvrRcAc0VUXayi9fxQFsMLyFplDE9vMkIJbP76AVpa-5-fxhNza3DqhX4tss4NlB49WPi_dA00Hw6O8qK5pDzdetYlhGgOyXOLBe7mTG9oJ5W0wcvQhEVoX9mpJk_SGeqRLWGA0dBSjYCZtiyrY8_B-DP53S7fbYwiSYtq-g7sQDXKVadRpGvUyKq7yxA0SLsU42uvoqSGb0qcd6udL1wbnTEkKmwNjNSb7xIUb-8PyE7DTPMt1ScJksn70sFQMJNkM2aK5M.x9_jYvKPnSibV1d0umUStZBxlSTSXrzVIFKqFzS8c-U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749299833%26sprefix%3Dr2d2%252Caps%252C548%26sr%3D8-1-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1">
|
||||
<div class="a-section aok-relative s-image-square-aspect"><img class="s-image"
|
||||
src="https://m.media-amazon.com/images/I/61kAC69zQUL._AC_UL320_.jpg"
|
||||
srcset="https://m.media-amazon.com/images/I/61kAC69zQUL._AC_UL320_.jpg 1x, https://m.media-amazon.com/images/I/61kAC69zQUL._AC_UL480_FMwebp_QL65_.jpg 1.5x, https://m.media-amazon.com/images/I/61kAC69zQUL._AC_UL640_FMwebp_QL65_.jpg 2x, https://m.media-amazon.com/images/I/61kAC69zQUL._AC_UL800_FMwebp_QL65_.jpg 2.5x, https://m.media-amazon.com/images/I/61kAC69zQUL._AC_UL960_FMwebp_QL65_.jpg 3x"
|
||||
alt="Sponsored Ad - Death Star BB8 R2D2 Golf Balls with 20 Printed tees • Great Gift IDEA from Moms, DADS and Kids -"
|
||||
aria-hidden="true" data-image-index="1" data-image-load=""
|
||||
data-image-latency="s-product-image" data-image-source-density="1">
|
||||
</div>
|
||||
</a></span></div>
|
||||
<div class="a-section a-spacing-small puis-padding-left-small puis-padding-right-small">
|
||||
<div data-cy="title-recipe"
|
||||
class="a-section a-spacing-none a-spacing-top-small s-title-instructions-style">
|
||||
<div class="a-row a-spacing-micro"><span class="a-declarative"
|
||||
data-version-id="v2dwi5hq8xzthf26x0gg1mcl2oj"
|
||||
data-render-id="r3o8bgr5zt3kmy2jv4su6fn4kyw" data-action="a-popover"
|
||||
data-csa-c-func-deps="aui-da-a-popover"
|
||||
data-a-popover="{"name":"sp-info-popover-B081XSYZMS","position":"triggerVertical","popoverLabel":"View Sponsored information or leave ad feedback","closeButtonLabel":"Close popup","closeButton":"true","dataStrategy":"preload"}"
|
||||
data-csa-c-type="widget" data-csa-c-id="wqddan-z1l67e-lissct-rciw65"><a
|
||||
href="javascript:void(0)" role="button" style="text-decoration: none;"
|
||||
class="puis-label-popover puis-sponsored-label-text"><span
|
||||
class="puis-label-popover-default"><span
|
||||
aria-label="View Sponsored information or leave ad feedback"
|
||||
class="a-color-secondary">Sponsored</span></span><span
|
||||
class="puis-label-popover-hover"><span aria-hidden="true"
|
||||
class="a-color-base">Sponsored</span></span> <span
|
||||
class="aok-inline-block puis-sponsored-label-info-icon"></span></a></span>
|
||||
<div class="a-popover-preload" id="a-popover-sp-info-popover-B081XSYZMS">
|
||||
<div class="puis puis-v2dwi5hq8xzthf26x0gg1mcl2oj"><span>You’re seeing this
|
||||
ad based on the product’s relevance to your search query.</span>
|
||||
<div class="a-row"><span class="a-declarative"
|
||||
data-version-id="v2dwi5hq8xzthf26x0gg1mcl2oj"
|
||||
data-render-id="r3o8bgr5zt3kmy2jv4su6fn4kyw"
|
||||
data-action="s-safe-ajax-modal-trigger"
|
||||
data-csa-c-func-deps="aui-da-s-safe-ajax-modal-trigger"
|
||||
data-s-safe-ajax-modal-trigger="{"header":"Leave feedback","dataStrategy":"ajax","ajaxUrl":"/af/sp-loom/feedback-form?pl=%7B%22adPlacementMetaData%22%3A%7B%22searchTerms%22%3A%22cjJkMg%3D%3D%22%2C%22pageType%22%3A%22Search%22%2C%22feedbackType%22%3A%22sponsoredProductsLoom%22%2C%22slotName%22%3A%22TOP%22%7D%2C%22adCreativeMetaData%22%3A%7B%22adProgramId%22%3A1024%2C%22adCreativeDetails%22%3A%5B%7B%22asin%22%3A%22B081XSYZMS%22%2C%22title%22%3A%22Death+Star+BB8+R2D2+Golf+Balls+with+20+Printed+tees+%E2%80%A2+Great+Gift+IDEA+from+Moms%2C+DADS+and+Kids+-%22%2C%22priceInfo%22%3A%7B%22amount%22%3A29.95%2C%22currencyCode%22%3A%22USD%22%7D%2C%22sku%22%3A%22starwars3pk20tees%22%2C%22adId%22%3A%22A03790291PREH7M3Q3SVS%22%2C%22campaignId%22%3A%22A01050612Q0SQZ2PTMGO9%22%2C%22advertiserIdNS%22%3Anull%2C%22selectionSignals%22%3Anull%7D%5D%7D%7D"}"
|
||||
data-csa-c-type="widget"
|
||||
data-csa-c-id="ygslsp-ir23ei-7k9x6z-73l1tp"><a
|
||||
class="a-link-normal s-underline-text s-underline-link-text s-link-style"
|
||||
href="#"><span>Leave ad feedback</span> </a> </span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><a class="a-link-normal s-line-clamp-4 s-link-style a-text-normal"
|
||||
href="/sspa/click?ie=UTF8&spc=MToxNzQwNTE0ODkzNDczNzk3OjE3NDkyOTk4MzM6c3BfYXRmOjIwMDA2NzY0ODgwMjc5ODo6MDo6&url=%2FDeath-Star-R2D2-Balls-Printed%2Fdp%2FB081XSYZMS%2Fref%3Dsr_1_1_sspa%3Fcrid%3D3C1EXMXN59Q9G%26dib%3DeyJ2IjoiMSJ9.7tBl5bhZh59L9qIPZUe9SLa2fy_HvzboxuQxvrRcAc0VUXayi9fxQFsMLyFplDE9vMkIJbP76AVpa-5-fxhNza3DqhX4tss4NlB49WPi_dA00Hw6O8qK5pDzdetYlhGgOyXOLBe7mTG9oJ5W0wcvQhEVoX9mpJk_SGeqRLWGA0dBSjYCZtiyrY8_B-DP53S7fbYwiSYtq-g7sQDXKVadRpGvUyKq7yxA0SLsU42uvoqSGb0qcd6udL1wbnTEkKmwNjNSb7xIUb-8PyE7DTPMt1ScJksn70sFQMJNkM2aK5M.x9_jYvKPnSibV1d0umUStZBxlSTSXrzVIFKqFzS8c-U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749299833%26sprefix%3Dr2d2%252Caps%252C548%26sr%3D8-1-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1">
|
||||
<h2 aria-label="Sponsored Ad - Death Star BB8 R2D2 Golf Balls with 20 Printed tees • Great Gift IDEA from Moms, DADS and Kids -"
|
||||
class="a-size-base-plus a-spacing-none a-color-base a-text-normal">
|
||||
<span>Death Star BB8 R2D2 Golf Balls with 20 Printed tees • Great Gift IDEA
|
||||
from Moms, DADS and Kids -</span></h2>
|
||||
</a>
|
||||
</div>
|
||||
<div data-cy="reviews-block" class="a-section a-spacing-none a-spacing-top-micro">
|
||||
<div class="a-row a-size-small"><span class="a-declarative"
|
||||
data-version-id="v2dwi5hq8xzthf26x0gg1mcl2oj"
|
||||
data-render-id="r3o8bgr5zt3kmy2jv4su6fn4kyw" data-action="a-popover"
|
||||
data-csa-c-func-deps="aui-da-a-popover"
|
||||
data-a-popover="{"position":"triggerBottom","popoverLabel":"4.7 out of 5 stars, rating details","url":"/review/widgets/average-customer-review/popover/ref=acr_search__popover?ie=UTF8&asin=B081XSYZMS&ref_=acr_search__popover&contextId=search","closeButton":true,"closeButtonLabel":""}"
|
||||
data-csa-c-type="widget" data-csa-c-id="oykdvt-8s1ebj-2kegf2-7ii7tp"><a
|
||||
aria-label="4.7 out of 5 stars, rating details"
|
||||
href="javascript:void(0)" role="button"
|
||||
class="a-popover-trigger a-declarative"><i
|
||||
data-cy="reviews-ratings-slot" aria-hidden="true"
|
||||
class="a-icon a-icon-star-small a-star-small-4-5"><span
|
||||
class="a-icon-alt">4.7 out of 5 stars</span></i><i
|
||||
class="a-icon a-icon-popover"></i></a></span> <span
|
||||
data-component-type="s-client-side-analytics" class="rush-component"
|
||||
data-version-id="v2dwi5hq8xzthf26x0gg1mcl2oj"
|
||||
data-render-id="r3o8bgr5zt3kmy2jv4su6fn4kyw" data-component-id="8">
|
||||
<div style="display: inline-block"
|
||||
class="s-csa-instrumentation-wrapper alf-search-csa-instrumentation-wrapper"
|
||||
data-csa-c-type="alf-af-component"
|
||||
data-csa-c-content-id="alf-customer-ratings-count-component"
|
||||
data-csa-c-slot-id="alf-reviews" data-csa-op-log-render=""
|
||||
data-csa-c-layout="GRID" data-csa-c-asin="B081XSYZMS"
|
||||
data-csa-c-id="6l5wc4-ngelan-hd9x4t-d4a2k7"><a aria-label="184 ratings"
|
||||
class="a-link-normal s-underline-text s-underline-link-text s-link-style"
|
||||
href="/sspa/click?ie=UTF8&spc=MToxNzQwNTE0ODkzNDczNzk3OjE3NDkyOTk4MzM6c3BfYXRmOjIwMDA2NzY0ODgwMjc5ODo6MDo6&url=%2FDeath-Star-R2D2-Balls-Printed%2Fdp%2FB081XSYZMS%2Fref%3Dsr_1_1_sspa%3Fcrid%3D3C1EXMXN59Q9G%26dib%3DeyJ2IjoiMSJ9.7tBl5bhZh59L9qIPZUe9SLa2fy_HvzboxuQxvrRcAc0VUXayi9fxQFsMLyFplDE9vMkIJbP76AVpa-5-fxhNza3DqhX4tss4NlB49WPi_dA00Hw6O8qK5pDzdetYlhGgOyXOLBe7mTG9oJ5W0wcvQhEVoX9mpJk_SGeqRLWGA0dBSjYCZtiyrY8_B-DP53S7fbYwiSYtq-g7sQDXKVadRpGvUyKq7yxA0SLsU42uvoqSGb0qcd6udL1wbnTEkKmwNjNSb7xIUb-8PyE7DTPMt1ScJksn70sFQMJNkM2aK5M.x9_jYvKPnSibV1d0umUStZBxlSTSXrzVIFKqFzS8c-U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749299833%26sprefix%3Dr2d2%252Caps%252C548%26sr%3D8-1-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1#customerReviews"><span
|
||||
aria-hidden="true"
|
||||
class="a-size-base s-underline-text">184</span> </a> </div>
|
||||
</span></div>
|
||||
<div class="a-row a-size-base"><span class="a-size-base a-color-secondary">50+
|
||||
bought in past month</span></div>
|
||||
</div>
|
||||
<div data-cy="price-recipe"
|
||||
class="a-section a-spacing-none a-spacing-top-small s-price-instructions-style">
|
||||
<div class="a-row a-size-base a-color-base">
|
||||
<div class="a-row"><span id="price-link" class="aok-offscreen">Price, product
|
||||
page</span><a aria-describedby="price-link"
|
||||
class="a-link-normal s-no-hover s-underline-text s-underline-link-text s-link-style a-text-normal"
|
||||
href="/sspa/click?ie=UTF8&spc=MToxNzQwNTE0ODkzNDczNzk3OjE3NDkyOTk4MzM6c3BfYXRmOjIwMDA2NzY0ODgwMjc5ODo6MDo6&url=%2FDeath-Star-R2D2-Balls-Printed%2Fdp%2FB081XSYZMS%2Fref%3Dsr_1_1_sspa%3Fcrid%3D3C1EXMXN59Q9G%26dib%3DeyJ2IjoiMSJ9.7tBl5bhZh59L9qIPZUe9SLa2fy_HvzboxuQxvrRcAc0VUXayi9fxQFsMLyFplDE9vMkIJbP76AVpa-5-fxhNza3DqhX4tss4NlB49WPi_dA00Hw6O8qK5pDzdetYlhGgOyXOLBe7mTG9oJ5W0wcvQhEVoX9mpJk_SGeqRLWGA0dBSjYCZtiyrY8_B-DP53S7fbYwiSYtq-g7sQDXKVadRpGvUyKq7yxA0SLsU42uvoqSGb0qcd6udL1wbnTEkKmwNjNSb7xIUb-8PyE7DTPMt1ScJksn70sFQMJNkM2aK5M.x9_jYvKPnSibV1d0umUStZBxlSTSXrzVIFKqFzS8c-U%26dib_tag%3Dse%26keywords%3Dr2d2%26qid%3D1749299833%26sprefix%3Dr2d2%252Caps%252C548%26sr%3D8-1-spons%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1"><span
|
||||
class="a-price" data-a-size="xl" data-a-color="base"><span
|
||||
class="a-offscreen">$29.95</span><span aria-hidden="true"><span
|
||||
class="a-price-symbol">$</span><span
|
||||
class="a-price-whole">29<span
|
||||
class="a-price-decimal">.</span></span><span
|
||||
class="a-price-fraction">95</span></span></span></a></div>
|
||||
<div class="a-row"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-cy="delivery-recipe" class="a-section a-spacing-none a-spacing-top-micro">
|
||||
<div class="a-row a-size-base a-color-secondary s-align-children-center"><span
|
||||
aria-label="FREE delivery Thu, Jun 19 to Malaysia on $49 of eligible items"><span
|
||||
class="a-color-base">FREE delivery </span><span
|
||||
class="a-color-base a-text-bold">Thu, Jun 19 </span><span
|
||||
class="a-color-base">to Malaysia on $49 of eligible items</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-cy="certification-recipe"
|
||||
class="a-section a-spacing-none a-spacing-top-micro">
|
||||
<div class="a-row">
|
||||
<div class="a-section a-spacing-none s-align-children-center">
|
||||
<div class="a-section a-spacing-none s-pc-faceout-container">
|
||||
<div>
|
||||
<div class="s-align-children-center"><span class="a-declarative"
|
||||
data-version-id="v2dwi5hq8xzthf26x0gg1mcl2oj"
|
||||
data-render-id="r3o8bgr5zt3kmy2jv4su6fn4kyw"
|
||||
data-action="s-pc-sidesheet-open"
|
||||
data-csa-c-func-deps="aui-da-s-pc-sidesheet-open"
|
||||
data-s-pc-sidesheet-open="{"preloadDomId":"pc-side-sheet-B081XSYZMS","popoverLabel":"Product certifications","interactLoggingMetricsList":["provenanceCertifications_desktop_sbe_badge"],"closeButtonLabel":"Close popup","dwellMetric":"provenanceCertifications_desktop_sbe_badge_t"}"
|
||||
data-csa-c-type="widget"
|
||||
data-csa-c-id="hdfxi6-bjlgup-5dql15-88t9ao"><a
|
||||
data-cy="s-pc-faceout-badge"
|
||||
class="a-link-normal s-no-underline s-pc-badge s-align-children-center aok-block"
|
||||
href="javascript:void(0)" role="button">
|
||||
<div
|
||||
class="a-section s-pc-attribute-pill-text s-margin-bottom-none s-margin-bottom-none aok-block s-pc-certification-faceout">
|
||||
<span class="faceout-image-view"></span><img alt=""
|
||||
src="https://m.media-amazon.com/images/I/111mHoVK0kL._SS200_.png"
|
||||
class="s-image" height="18px" width="18px">
|
||||
<span class="a-size-base a-color-base">Small
|
||||
Business</span>
|
||||
<div
|
||||
class="s-margin-bottom-none s-pc-sidesheet-chevron aok-nowrap">
|
||||
<i class="a-icon a-icon-popover aok-align-center"
|
||||
role="presentation"></i></div>
|
||||
</div>
|
||||
</a></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pc-side-sheet-B081XSYZMS"
|
||||
class="a-section puis puis-v2dwi5hq8xzthf26x0gg1mcl2oj aok-hidden">
|
||||
<div class="a-section s-pc-container-side-sheet">
|
||||
<div class="s-align-children-center a-spacing-small">
|
||||
<div class="s-align-children-center s-pc-certification"
|
||||
role="heading" aria-level="2"><span
|
||||
class="faceout-image-view"></span>
|
||||
<div alt="" style="height: 24px; width: 24px;"
|
||||
class="a-image-wrapper a-lazy-loaded a-manually-loaded s-image"
|
||||
data-a-image-source="https://m.media-amazon.com/images/I/111mHoVK0kL._SS200_.png">
|
||||
<noscript><img alt=""
|
||||
src="https://m.media-amazon.com/images/I/111mHoVK0kL._SS200_.png"
|
||||
height="24px" width="24px" /></noscript></div> <span
|
||||
class="a-size-medium-plus a-color-base a-text-bold">Small
|
||||
Business</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-spacing-medium s-pc-link-container"><span
|
||||
class="a-size-base a-color-secondary">Shop products from small
|
||||
business brands sold in Amazon’s store. Discover more about the
|
||||
small businesses partnering with Amazon and Amazon’s commitment
|
||||
to empowering them.</span> <a
|
||||
class="a-size-base a-link-normal s-link-style"
|
||||
href="https://www.amazon.com/b/ref=s9_acss_bw_cg_sbp22c_1e1_w/ref=SBE_navbar_5?pf_rd_r=6W5X52VNZRB7GK1E1VX2&pf_rd_p=56621c3d-cff4-45e1-9bf4-79bbeb8006fc&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=merchandised-search-top-3&pf_rd_t=30901&pf_rd_i=17879387011&node=18018208011">Learn
|
||||
more</a> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
89
docs/examples/c4a_script/generate_script_hello_world.py
Normal file
89
docs/examples/c4a_script/generate_script_hello_world.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hello World Example: LLM-Generated C4A-Script
|
||||
|
||||
This example shows how to use the new generate_script() function to automatically
|
||||
create C4A-Script automation from natural language descriptions and HTML.
|
||||
"""
|
||||
|
||||
from crawl4ai.script.c4a_compile import C4ACompiler
|
||||
|
||||
def main():
|
||||
print("🤖 C4A-Script Generation Hello World")
|
||||
print("=" * 50)
|
||||
|
||||
# Example 1: Simple login form
|
||||
html = """
|
||||
<html>
|
||||
<body>
|
||||
<form id="login">
|
||||
<input id="email" type="email" placeholder="Email">
|
||||
<input id="password" type="password" placeholder="Password">
|
||||
<button id="submit">Login</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
goal = "Fill in email 'user@example.com', password 'secret123', and submit the form"
|
||||
|
||||
print("📝 Goal:", goal)
|
||||
print("🌐 HTML: Simple login form")
|
||||
print()
|
||||
|
||||
# Generate C4A-Script
|
||||
print("🔧 Generated C4A-Script:")
|
||||
print("-" * 30)
|
||||
c4a_script = C4ACompiler.generate_script(
|
||||
html=html,
|
||||
query=goal,
|
||||
mode="c4a"
|
||||
)
|
||||
print(c4a_script)
|
||||
print()
|
||||
|
||||
# Generate JavaScript
|
||||
print("🔧 Generated JavaScript:")
|
||||
print("-" * 30)
|
||||
js_script = C4ACompiler.generate_script(
|
||||
html=html,
|
||||
query=goal,
|
||||
mode="js"
|
||||
)
|
||||
print(js_script)
|
||||
print()
|
||||
|
||||
# Example 2: Simple button click
|
||||
html2 = """
|
||||
<html>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1>Welcome!</h1>
|
||||
<button id="start-btn" class="primary">Get Started</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
goal2 = "Click the 'Get Started' button"
|
||||
|
||||
print("=" * 50)
|
||||
print("📝 Goal:", goal2)
|
||||
print("🌐 HTML: Simple button")
|
||||
print()
|
||||
|
||||
print("🔧 Generated C4A-Script:")
|
||||
print("-" * 30)
|
||||
c4a_script2 = C4ACompiler.generate_script(
|
||||
html=html2,
|
||||
query=goal2,
|
||||
mode="c4a"
|
||||
)
|
||||
print(c4a_script2)
|
||||
print()
|
||||
|
||||
print("✅ Done! The LLM automatically converted natural language goals")
|
||||
print(" into executable automation scripts.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,111 @@
|
||||
[
|
||||
{
|
||||
"repository_name": "unclecode/crawl4ai",
|
||||
"repository_owner": "unclecode/crawl4ai",
|
||||
"repository_url": "/unclecode/crawl4ai",
|
||||
"description": "\ud83d\ude80\ud83e\udd16Crawl4AI: Open-source LLM Friendly Web Crawler & Scraper. Don't be shy, join here:https://discord.gg/jP8KfhDhyN",
|
||||
"primary_language": "Python",
|
||||
"star_count": "45.1k",
|
||||
"topics": [],
|
||||
"last_updated": "23 hours ago"
|
||||
},
|
||||
{
|
||||
"repository_name": "coleam00/mcp-crawl4ai-rag",
|
||||
"repository_owner": "coleam00/mcp-crawl4ai-rag",
|
||||
"repository_url": "/coleam00/mcp-crawl4ai-rag",
|
||||
"description": "Web Crawling and RAG Capabilities for AI Agents and AI Coding Assistants",
|
||||
"primary_language": "Python",
|
||||
"star_count": "748",
|
||||
"topics": [],
|
||||
"last_updated": "yesterday"
|
||||
},
|
||||
{
|
||||
"repository_name": "pdichone/crawl4ai-rag-system",
|
||||
"repository_owner": "pdichone/crawl4ai-rag-system",
|
||||
"repository_url": "/pdichone/crawl4ai-rag-system",
|
||||
"primary_language": "Python",
|
||||
"star_count": "44",
|
||||
"topics": [],
|
||||
"last_updated": "on 21 Jan"
|
||||
},
|
||||
{
|
||||
"repository_name": "weidwonder/crawl4ai-mcp-server",
|
||||
"repository_owner": "weidwonder/crawl4ai-mcp-server",
|
||||
"repository_url": "/weidwonder/crawl4ai-mcp-server",
|
||||
"description": "\u7528\u4e8e\u63d0\u4f9b\u7ed9\u672c\u5730\u5f00\u53d1\u8005\u7684 LLM\u7684\u9ad8\u6548\u4e92\u8054\u7f51\u641c\u7d22&\u5185\u5bb9\u83b7\u53d6\u7684MCP Server\uff0c \u8282\u7701\u4f60\u7684token",
|
||||
"primary_language": "Python",
|
||||
"star_count": "87",
|
||||
"topics": [],
|
||||
"last_updated": "24 days ago"
|
||||
},
|
||||
{
|
||||
"repository_name": "leonardogrig/crawl4ai-deepseek-example",
|
||||
"repository_owner": "leonardogrig/crawl4ai-deepseek-example",
|
||||
"repository_url": "/leonardogrig/crawl4ai-deepseek-example",
|
||||
"primary_language": "Python",
|
||||
"star_count": "29",
|
||||
"topics": [],
|
||||
"last_updated": "on 18 Jan"
|
||||
},
|
||||
{
|
||||
"repository_name": "laurentvv/crawl4ai-mcp",
|
||||
"repository_owner": "laurentvv/crawl4ai-mcp",
|
||||
"repository_url": "/laurentvv/crawl4ai-mcp",
|
||||
"description": "Web crawling tool that integrates with AI assistants via the MCP",
|
||||
"primary_language": "Python",
|
||||
"star_count": "10",
|
||||
"topics": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
],
|
||||
"last_updated": "on 16 Mar"
|
||||
},
|
||||
{
|
||||
"repository_name": "kaymen99/ai-web-scraper",
|
||||
"repository_owner": "kaymen99/ai-web-scraper",
|
||||
"repository_url": "/kaymen99/ai-web-scraper",
|
||||
"description": "AI web scraper built withCrawl4AIfor extracting structured leads data from websites.",
|
||||
"primary_language": "Python",
|
||||
"star_count": "30",
|
||||
"topics": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
],
|
||||
"last_updated": "on 13 Feb"
|
||||
},
|
||||
{
|
||||
"repository_name": "atakkant/ai_web_crawler",
|
||||
"repository_owner": "atakkant/ai_web_crawler",
|
||||
"repository_url": "/atakkant/ai_web_crawler",
|
||||
"description": "crawl4ai, DeepSeek, Groq",
|
||||
"primary_language": "Python",
|
||||
"star_count": "9",
|
||||
"topics": [],
|
||||
"last_updated": "on 19 Feb"
|
||||
},
|
||||
{
|
||||
"repository_name": "Croups/auto-scraper-with-llms",
|
||||
"repository_owner": "Croups/auto-scraper-with-llms",
|
||||
"repository_url": "/Croups/auto-scraper-with-llms",
|
||||
"description": "Web scraping AI that leverages thecrawl4ailibrary to extract structured data from web pages using various large language models (LLMs).",
|
||||
"primary_language": "Python",
|
||||
"star_count": "49",
|
||||
"topics": [],
|
||||
"last_updated": "on 8 Apr"
|
||||
},
|
||||
{
|
||||
"repository_name": "leonardogrig/crawl4ai_llm_examples",
|
||||
"repository_owner": "leonardogrig/crawl4ai_llm_examples",
|
||||
"repository_url": "/leonardogrig/crawl4ai_llm_examples",
|
||||
"primary_language": "Python",
|
||||
"star_count": "8",
|
||||
"topics": [],
|
||||
"last_updated": "on 29 Jan"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "GitHub Repository Cards",
|
||||
"baseSelector": "div.Box-sc-g0xbh4-0.iwUbcA",
|
||||
"fields": [
|
||||
{
|
||||
"name": "repository_name",
|
||||
"selector": "div.search-title a span",
|
||||
"type": "text",
|
||||
"transform": "strip"
|
||||
},
|
||||
{
|
||||
"name": "repository_owner",
|
||||
"selector": "div.search-title a span",
|
||||
"type": "text",
|
||||
"transform": "split",
|
||||
"pattern": "/"
|
||||
},
|
||||
{
|
||||
"name": "repository_url",
|
||||
"selector": "div.search-title a",
|
||||
"type": "attribute",
|
||||
"attribute": "href",
|
||||
"transform": "prepend",
|
||||
"pattern": "https://github.com"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"selector": "div.dcdlju span",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "primary_language",
|
||||
"selector": "ul.bZkODq li span[aria-label]",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "star_count",
|
||||
"selector": "ul.bZkODq li a[href*='stargazers'] span",
|
||||
"type": "text",
|
||||
"transform": "strip"
|
||||
},
|
||||
{
|
||||
"name": "topics",
|
||||
"type": "list",
|
||||
"selector": "div.jgRnBg div a",
|
||||
"fields": [
|
||||
{
|
||||
"name": "topic_name",
|
||||
"selector": "a",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "last_updated",
|
||||
"selector": "ul.bZkODq li span[title]",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "has_sponsor_button",
|
||||
"selector": "button[aria-label*='Sponsor']",
|
||||
"type": "text",
|
||||
"transform": "exists"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
(async () => {
|
||||
const waitForElement = (selector, timeout = 10000) => new Promise((resolve, reject) => {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) return resolve(el);
|
||||
const observer = new MutationObserver(() => {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
observer.disconnect();
|
||||
resolve(el);
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
reject(new Error(`Timeout waiting for ${selector}`));
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
try {
|
||||
const searchInput = await waitForElement('#adv_code_search input[type="text"]');
|
||||
searchInput.value = 'crawl4AI';
|
||||
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
const languageSelect = await waitForElement('#search_language');
|
||||
languageSelect.value = 'Python';
|
||||
languageSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
const starsInput = await waitForElement('#search_stars');
|
||||
starsInput.value = '>10000';
|
||||
starsInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
const searchButton = await waitForElement('#adv_code_search button[type="submit"]');
|
||||
searchButton.click();
|
||||
|
||||
await waitForElement('.codesearch-results, #search-results');
|
||||
} catch (e) {
|
||||
console.error('Search script failed:', e.message);
|
||||
}
|
||||
})();
|
||||
211
docs/examples/c4a_script/github_search/github_search_crawler.py
Normal file
211
docs/examples/c4a_script/github_search/github_search_crawler.py
Normal file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GitHub Advanced Search Example using Crawl4AI
|
||||
|
||||
This example demonstrates:
|
||||
1. Using LLM to generate C4A-Script from HTML snippets
|
||||
2. Single arun() call with navigation, search form filling, and extraction
|
||||
3. JSON CSS extraction for structured repository data
|
||||
4. Complete workflow: navigate → fill form → submit → extract results
|
||||
|
||||
Requirements:
|
||||
- Crawl4AI with generate_script support
|
||||
- LLM API key (configured in environment)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode
|
||||
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy
|
||||
from crawl4ai.script.c4a_compile import C4ACompiler
|
||||
|
||||
|
||||
class GitHubSearchScraper:
|
||||
def __init__(self):
|
||||
self.base_dir = Path(__file__).parent
|
||||
self.search_script_path = self.base_dir / "generated_search_script.js"
|
||||
self.schema_path = self.base_dir / "generated_result_schema.json"
|
||||
self.results_path = self.base_dir / "extracted_repositories.json"
|
||||
self.session_id = "github_search_session"
|
||||
|
||||
async def generate_search_script(self) -> str:
|
||||
"""Generate JavaScript for GitHub advanced search interaction"""
|
||||
print("🔧 Generating search script from search_form.html...")
|
||||
|
||||
# Check if already generated
|
||||
if self.search_script_path.exists():
|
||||
print("✅ Using cached search script")
|
||||
return self.search_script_path.read_text()
|
||||
|
||||
# Read the search form HTML
|
||||
search_form_html = (self.base_dir / "search_form.html").read_text()
|
||||
|
||||
# Generate script using LLM
|
||||
search_goal = """
|
||||
Search for crawl4AI repositories written in Python with more than 10000 stars:
|
||||
1. Wait for the main search input to be visible
|
||||
2. Type "crawl4AI" into the main search box
|
||||
3. Select "Python" from the language dropdown (#search_language)
|
||||
4. Type ">10000" into the stars input field (#search_stars)
|
||||
5. Click the search button to submit the form
|
||||
6. Wait for the search results to appear
|
||||
"""
|
||||
|
||||
try:
|
||||
script = C4ACompiler.generate_script(
|
||||
html=search_form_html,
|
||||
query=search_goal,
|
||||
mode="js"
|
||||
)
|
||||
|
||||
# Save for future use
|
||||
self.search_script_path.write_text(script)
|
||||
print("✅ Search script generated and saved!")
|
||||
print(f"📄 Script preview:\n{script[:500]}...")
|
||||
return script
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error generating search script: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def generate_result_schema(self) -> Dict[str, Any]:
|
||||
"""Generate JSON CSS extraction schema from result HTML"""
|
||||
print("\n🔧 Generating result extraction schema...")
|
||||
|
||||
# Check if already generated
|
||||
if self.schema_path.exists():
|
||||
print("✅ Using cached extraction schema")
|
||||
return json.loads(self.schema_path.read_text())
|
||||
|
||||
# Read the result HTML
|
||||
result_html = (self.base_dir / "result.html").read_text()
|
||||
|
||||
# Generate extraction schema using LLM
|
||||
schema_goal = """
|
||||
Create a JSON CSS extraction schema to extract from each repository card:
|
||||
- Repository name (the repository name only, not including owner)
|
||||
- Repository owner (organization or username)
|
||||
- Repository URL (full GitHub URL)
|
||||
- Description
|
||||
- Primary programming language
|
||||
- Star count (numeric value)
|
||||
- Topics/tags (array of topic names)
|
||||
- Last updated (time ago string)
|
||||
- Whether it has a sponsor button
|
||||
|
||||
The schema should handle multiple repository results on the search results page.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Generate schema
|
||||
schema = JsonCssExtractionStrategy.generate_schema(
|
||||
html=result_html,
|
||||
query=schema_goal,
|
||||
)
|
||||
|
||||
# Save for future use
|
||||
self.schema_path.write_text(json.dumps(schema, indent=2))
|
||||
print("✅ Extraction schema generated and saved!")
|
||||
print(f"📄 Schema fields: {[f['name'] for f in schema['fields']]}")
|
||||
return schema
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error generating schema: {e}")
|
||||
raise
|
||||
|
||||
async def crawl_github(self):
|
||||
"""Main crawling logic with single arun() call"""
|
||||
print("\n🚀 Starting GitHub repository search...")
|
||||
|
||||
# Generate scripts and schemas
|
||||
search_script = await self.generate_search_script()
|
||||
result_schema = await self.generate_result_schema()
|
||||
|
||||
# Configure browser (headless=False to see the action)
|
||||
browser_config = BrowserConfig(
|
||||
headless=False,
|
||||
verbose=True,
|
||||
viewport_width=1920,
|
||||
viewport_height=1080
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||
print("\n📍 Navigating to GitHub advanced search and executing search...")
|
||||
|
||||
# Single call: Navigate, execute search, and extract results
|
||||
search_config = CrawlerRunConfig(
|
||||
session_id=self.session_id,
|
||||
js_code=search_script, # Execute generated JS
|
||||
# wait_for="[data-testid='results-list']", # Wait for search results
|
||||
wait_for=".Box-sc-g0xbh4-0.iwUbcA", # Wait for search results
|
||||
extraction_strategy=JsonCssExtractionStrategy(schema=result_schema),
|
||||
delay_before_return_html=3.0, # Give time for results to fully load
|
||||
cache_mode=CacheMode.BYPASS # Don't cache for fresh results
|
||||
)
|
||||
|
||||
result = await crawler.arun(
|
||||
url="https://github.com/search/advanced",
|
||||
config=search_config
|
||||
)
|
||||
|
||||
if not result.success:
|
||||
print("❌ Failed to search GitHub")
|
||||
print(f"Error: {result.error_message}")
|
||||
return
|
||||
|
||||
print("✅ Search and extraction completed successfully!")
|
||||
|
||||
# Extract and save results
|
||||
if result.extracted_content:
|
||||
repositories = json.loads(result.extracted_content)
|
||||
print(f"\n🔍 Found {len(repositories)} repositories matching criteria")
|
||||
|
||||
# Save results
|
||||
self.results_path.write_text(
|
||||
json.dumps(repositories, indent=2)
|
||||
)
|
||||
print(f"💾 Results saved to: {self.results_path}")
|
||||
|
||||
# Print sample results
|
||||
print("\n📊 Sample Results:")
|
||||
for i, repo in enumerate(repositories[:5], 1):
|
||||
print(f"\n{i}. {repo.get('owner', 'Unknown')}/{repo.get('name', 'Unknown')}")
|
||||
print(f" Description: {repo.get('description', 'No description')[:80]}...")
|
||||
print(f" Language: {repo.get('language', 'Unknown')}")
|
||||
print(f" Stars: {repo.get('stars', 'Unknown')}")
|
||||
print(f" Updated: {repo.get('last_updated', 'Unknown')}")
|
||||
if repo.get('topics'):
|
||||
print(f" Topics: {', '.join(repo['topics'][:5])}")
|
||||
print(f" URL: {repo.get('url', 'Unknown')}")
|
||||
|
||||
else:
|
||||
print("❌ No repositories extracted")
|
||||
|
||||
# Save screenshot for reference
|
||||
if result.screenshot:
|
||||
screenshot_path = self.base_dir / "search_results_screenshot.png"
|
||||
with open(screenshot_path, "wb") as f:
|
||||
f.write(result.screenshot)
|
||||
print(f"\n📸 Screenshot saved to: {screenshot_path}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the GitHub search scraper"""
|
||||
scraper = GitHubSearchScraper()
|
||||
await scraper.crawl_github()
|
||||
|
||||
print("\n🎉 GitHub search example completed!")
|
||||
print("Check the generated files:")
|
||||
print(" - generated_search_script.js")
|
||||
print(" - generated_result_schema.json")
|
||||
print(" - extracted_repositories.json")
|
||||
print(" - search_results_screenshot.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
54
docs/examples/c4a_script/github_search/result.html
Normal file
54
docs/examples/c4a_script/github_search/result.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="Box-sc-g0xbh4-0 iwUbcA"><div class="Box-sc-g0xbh4-0 cSURfY"><div class="Box-sc-g0xbh4-0 gPrlij"><h3 class="Box-sc-g0xbh4-0 cvnppv"><div class="Box-sc-g0xbh4-0 kYLlPM"><div class="Box-sc-g0xbh4-0 eurdCD"><img data-component="Avatar" class="prc-Avatar-Avatar-ZRS-m" alt="" data-square="" width="20" height="20" src="https://github.com/TheAlgorithms.png?size=40" data-testid="github-avatar" style="--avatarSize-regular: 20px;"></div><div class="Box-sc-g0xbh4-0 MHoGG search-title"><a class="prc-Link-Link-85e08" href="/TheAlgorithms/Python"><span class="Box-sc-g0xbh4-0 kzfhBO search-match prc-Text-Text-0ima0">TheAlgorithms/<em>Python</em></span></a></div></div></h3><div class="Box-sc-g0xbh4-0 dcdlju"><span class="Box-sc-g0xbh4-0 gKFdvh search-match prc-Text-Text-0ima0">All Algorithms implemented in <em>Python</em></span></div><div class="Box-sc-g0xbh4-0 jgRnBg"><div><a class="Box-sc-g0xbh4-0 hIVEGR prc-Link-Link-85e08" href="/topics/python">python</a></div><div><a class="Box-sc-g0xbh4-0 hIVEGR prc-Link-Link-85e08" href="/topics/education">education</a></div><div><a class="Box-sc-g0xbh4-0 hIVEGR prc-Link-Link-85e08" href="/topics/algorithm">algorithm</a></div><div><a class="Box-sc-g0xbh4-0 hIVEGR prc-Link-Link-85e08" href="/topics/practice">practice</a></div><div><a class="Box-sc-g0xbh4-0 hIVEGR prc-Link-Link-85e08" href="/topics/interview">interview</a></div></div><ul class="Box-sc-g0xbh4-0 bZkODq"><li class="Box-sc-g0xbh4-0 eCfCAC"><div class="Box-sc-g0xbh4-0 hjDqIa"><div class="Box-sc-g0xbh4-0 fwSYsx"></div></div><span aria-label="Python language">Python</span></li><span class="Box-sc-g0xbh4-0 eXQoFa prc-Text-Text-0ima0" aria-hidden="true">·</span><li class="Box-sc-g0xbh4-0 eCfCAC"><a class="Box-sc-g0xbh4-0 iPuHRc prc-Link-Link-85e08" href="/TheAlgorithms/Python/stargazers" aria-label="201161 stars"><svg aria-hidden="true" focusable="false" class="octicon octicon-star Octicon-sc-9kayk9-0 kHVtWu" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path></svg><span class="prc-Text-Text-0ima0">201k</span></a></li><span class="Box-sc-g0xbh4-0 eXQoFa prc-Text-Text-0ima0" aria-hidden="true">·</span><li class="Box-sc-g0xbh4-0 eCfCAC"><span>Updated <div title="3 Jun 2025, 01:57 GMT+8" class="Truncate__StyledTruncate-sc-23o1d2-0 liVpTx"><span class="prc-Text-Text-0ima0" title="3 Jun 2025, 01:57 GMT+8">4 days ago</span></div></span></li></ul></div><div class="Box-sc-g0xbh4-0 gtlRHe"><div class="Box-sc-g0xbh4-0 fvaNTI"><button type="button" class="prc-Button-ButtonBase-c50BI" data-loading="false" data-size="small" data-variant="default" aria-describedby=":r1c:-loading-announcement"><span data-component="buttonContent" data-align="center" class="prc-Button-ButtonContent-HKbr-"><span data-component="leadingVisual" class="prc-Button-Visual-2epfX prc-Button-VisualWrap-Db-eB"><svg aria-hidden="true" focusable="false" class="octicon octicon-star" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path></svg></span><span data-component="text" class="prc-Button-Label-pTQ3x">Star</span></span></button></div><div class="Box-sc-g0xbh4-0 llZEgI"><div class="Box-sc-g0xbh4-0"> <button id="dialog-show-funding-links-modal-TheAlgorithms-Python" aria-label="Sponsor TheAlgorithms/Python" data-show-dialog-id="funding-links-modal-TheAlgorithms-Python" type="button" data-view-component="true" class="Button--secondary Button--small Button"> <span class="Button-content">
|
||||
<span class="Button-label"><svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-heart icon-sponsor mr-1 color-fg-sponsors">
|
||||
<path d="m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002ZM4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.58 20.58 0 0 0 8 13.393a20.58 20.58 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5Z"></path>
|
||||
</svg> <span data-view-component="true">Sponsor</span></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<dialog-helper>
|
||||
<dialog id="funding-links-modal-TheAlgorithms-Python" aria-modal="true" aria-labelledby="funding-links-modal-TheAlgorithms-Python-title" aria-describedby="funding-links-modal-TheAlgorithms-Python-description" data-view-component="true" class="Overlay Overlay-whenNarrow Overlay--size-medium Overlay--motion-scaleFade Overlay--disableScroll">
|
||||
<div data-view-component="true" class="Overlay-header">
|
||||
<div class="Overlay-headerContentWrap">
|
||||
<div class="Overlay-titleWrap">
|
||||
<h1 class="Overlay-title " id="funding-links-modal-TheAlgorithms-Python-title">
|
||||
Sponsor TheAlgorithms/Python
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
<div class="Overlay-actionWrap">
|
||||
<button data-close-dialog-id="funding-links-modal-TheAlgorithms-Python" aria-label="Close" type="button" data-view-component="true" class="close-button Overlay-closeButton"><svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-x">
|
||||
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path>
|
||||
</svg></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<scrollable-region data-labelled-by="funding-links-modal-TheAlgorithms-Python-title" data-catalyst="" style="overflow: auto;">
|
||||
<div data-view-component="true" class="Overlay-body"> <div class="text-left f5">
|
||||
<div class="pt-3 color-bg-overlay">
|
||||
<h5 class="flex-auto mb-3 mt-0">External links</h5>
|
||||
<div class="d-flex mb-3">
|
||||
<div class="circle mr-2 border d-flex flex-justify-center flex-items-center flex-shrink-0" style="width:24px;height:24px;">
|
||||
<img width="16" height="16" class="octicon rounded-2 d-block" alt="liberapay" src="https://github.githubassets.com/assets/liberapay-48108ded7267.svg">
|
||||
</div>
|
||||
<div class="flex-auto min-width-0">
|
||||
<a target="_blank" data-ga-click="Dashboard, click, Nav menu - item:org-profile context:organization" data-hydro-click="{"event_type":"sponsors.repo_funding_links_link_click","payload":{"platform":{"platform_type":"LIBERAPAY","platform_url":"https://liberapay.com/TheAlgorithms"},"platforms":[{"platform_type":"LIBERAPAY","platform_url":"https://liberapay.com/TheAlgorithms"}],"repo_id":63476337,"owner_id":20487725,"user_id":12494079,"originating_url":"https://github.com/TheAlgorithms/Python/funding_links?fragment=1"}}" data-hydro-click-hmac="123b5aa7d5ffff5ef0530f8e7fbaebcb564e8de1af26f1b858a19b0e1d4f9e5f" href="https://liberapay.com/TheAlgorithms"><span>liberapay.com/<strong>TheAlgorithms</strong></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="text-small p-3 border-top">
|
||||
<p class="my-0">
|
||||
<a class="Link--inTextBlock" href="https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository">Learn more about funding links in repositories</a>.
|
||||
</p>
|
||||
<p class="my-0">
|
||||
<a class="Link--secondary" href="/contact/report-abuse?report=TheAlgorithms%2FPython+%28Repository+Funding+Links%29">Report abuse</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</scrollable-region>
|
||||
|
||||
</dialog></dialog-helper>
|
||||
</div></div></div></div></div>
|
||||
336
docs/examples/c4a_script/github_search/search_form.html
Normal file
336
docs/examples/c4a_script/github_search/search_form.html
Normal file
@@ -0,0 +1,336 @@
|
||||
<form id="search_form" class="search_repos" data-turbo="false" action="/search" accept-charset="UTF-8" method="get">
|
||||
|
||||
<div class="pagehead codesearch-head color-border-muted">
|
||||
<div class="container-lg p-responsive d-flex flex-column flex-md-row">
|
||||
<h1 class="flex-shrink-0" id="search-title">Advanced search</h1>
|
||||
<div class="search-form-fluid flex-auto d-flex flex-column flex-md-row pt-2 pt-md-0" id="adv_code_search">
|
||||
<div class="flex-auto pr-md-2">
|
||||
<label class="form-control search-page-label js-advanced-search-label">
|
||||
<input aria-labelledby="search-title" class="form-control input-block search-page-input js-advanced-search-input js-advanced-search-prefix" data-search-prefix="" type="text" value="">
|
||||
<p class="completed-query js-advanced-query top-0 right-0 left-0"><span></span> </p>
|
||||
</label>
|
||||
<input class="js-search-query" type="hidden" name="q" value="">
|
||||
<input class="js-type-value" type="hidden" name="type" value="Repositories">
|
||||
<input type="hidden" name="ref" value="advsearch">
|
||||
</div>
|
||||
<div class="d-flex d-md-block flex-shrink-0 pt-2 pt-md-0">
|
||||
<button type="submit" data-view-component="true" class="btn flex-auto"> Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-lg p-responsive advanced-search-form">
|
||||
<fieldset class="pb-3 mb-4 border-bottom color-border-muted min-width-0">
|
||||
<h3>Advanced options</h3>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_from">From these owners</label></dt>
|
||||
<dd><input id="search_from" type="text" class="form-control js-advanced-search-prefix" placeholder="github, atom, electron, octokit" data-search-prefix="user:"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_repos">In these repositories</label></dt>
|
||||
<dd><input id="search_repos" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="twbs/bootstrap, rails/rails" data-search-prefix="repo:"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_date">Created on the dates</label></dt>
|
||||
<dd><input id="search_date" type="text" class="form-control js-advanced-search-prefix" value="" placeholder=">YYYY-MM-DD, YYYY-MM-DD" data-search-prefix="created:"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_language">Written in this language</label></dt>
|
||||
<dd>
|
||||
<select id="search_language" name="l" class="form-select js-advanced-search-prefix" data-search-prefix="language:">
|
||||
<option value="">Any language</option>
|
||||
<optgroup label="Popular">
|
||||
<option value="C">C</option>
|
||||
<option value="C#">C#</option>
|
||||
<option value="C++">C++</option>
|
||||
<option value="CoffeeScript">CoffeeScript</option>
|
||||
<option value="CSS">CSS</option>
|
||||
<option value="Dart">Dart</option>
|
||||
<option value="DM">DM</option>
|
||||
<option value="Elixir">Elixir</option>
|
||||
<option value="Go">Go</option>
|
||||
<option value="Groovy">Groovy</option>
|
||||
<option value="HTML">HTML</option>
|
||||
<option value="Java">Java</option>
|
||||
<option value="JavaScript">JavaScript</option>
|
||||
<option value="Kotlin">Kotlin</option>
|
||||
<option value="Objective-C">Objective-C</option>
|
||||
<option value="Perl">Perl</option>
|
||||
<option value="PHP">PHP</option>
|
||||
<option value="PowerShell">PowerShell</option>
|
||||
<option value="Python">Python</option>
|
||||
<option value="Ruby">Ruby</option>
|
||||
<option value="Rust">Rust</option>
|
||||
<option value="Scala">Scala</option>
|
||||
<option value="Shell">Shell</option>
|
||||
<option value="Swift">Swift</option>
|
||||
<option value="TypeScript">TypeScript</option>
|
||||
</optgroup>
|
||||
<optgroup label="Everything else">
|
||||
<option value="1C Enterprise">1C Enterprise</option>
|
||||
<option value="2-Dimensional Array">2-Dimensional Array</option>
|
||||
<option value="4D">4D</option>
|
||||
<option value="ABAP">ABAP</option>
|
||||
<option value="ABAP CDS">ABAP CDS</option>
|
||||
<option value="ABNF">ABNF</option>
|
||||
<option value="ActionScript">ActionScript</option>
|
||||
<option value="Ada">Ada</option>
|
||||
<option value="Adblock Filter List">Adblock Filter List</option>
|
||||
<option value="Adobe Font Metrics">Adobe Font Metrics</option>
|
||||
<option value="Agda">Agda</option>
|
||||
<option value="AGS Script">AGS Script</option>
|
||||
<option value="AIDL">AIDL</option>
|
||||
<option value="Aiken">Aiken</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</dd>
|
||||
</dl>
|
||||
</fieldset>
|
||||
<fieldset class="pb-3 mb-4 border-bottom color-border-muted min-width-0">
|
||||
<h3>Repositories options</h3>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_stars">With this many stars</label></dt>
|
||||
<dd><input id="search_stars" type="text" class="form-control js-advanced-search-prefix" placeholder="0..100, 200, >1000" data-search-prefix="stars:" data-search-type="Repositories"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_forks">With this many forks</label></dt>
|
||||
<dd><input id="search_forks" type="text" class="form-control js-advanced-search-prefix" placeholder="50..100, 200, <5" data-search-prefix="forks:" data-search-type="Repositories"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_size">Of this size</label></dt>
|
||||
<dd><input id="search_size" type="text" class="form-control js-advanced-search-prefix" placeholder="Repository size in KB" data-search-prefix="size:" data-search-type="Repositories"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_push">Pushed to</label></dt>
|
||||
<dd><input id="search_push" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="<YYYY-MM-DD" data-search-prefix="pushed:" data-search-type="Repositories"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_license">With this license</label></dt>
|
||||
<dd>
|
||||
<select id="search_license" class="form-select js-advanced-search-prefix" data-search-prefix="license:" data-search-type="Repositories">
|
||||
<option value="">Any license</option>
|
||||
<optgroup label="Licenses">
|
||||
<option value="0bsd">BSD Zero Clause License</option>
|
||||
<option value="afl-3.0">Academic Free License v3.0</option>
|
||||
<option value="agpl-3.0">GNU Affero General Public License v3.0</option>
|
||||
<option value="apache-2.0">Apache License 2.0</option>
|
||||
<option value="artistic-2.0">Artistic License 2.0</option>
|
||||
<option value="blueoak-1.0.0">Blue Oak Model License 1.0.0</option>
|
||||
<option value="bsd-2-clause">BSD 2-Clause "Simplified" License</option>
|
||||
<option value="bsd-2-clause-patent">BSD-2-Clause Plus Patent License</option>
|
||||
<option value="bsd-3-clause">BSD 3-Clause "New" or "Revised" License</option>
|
||||
<option value="bsd-3-clause-clear">BSD 3-Clause Clear License</option>
|
||||
<option value="bsd-4-clause">BSD 4-Clause "Original" or "Old" License</option>
|
||||
<option value="bsl-1.0">Boost Software License 1.0</option>
|
||||
<option value="cc-by-4.0">Creative Commons Attribution 4.0 International</option>
|
||||
<option value="cc-by-sa-4.0">Creative Commons Attribution Share Alike 4.0 International</option>
|
||||
<option value="cc0-1.0">Creative Commons Zero v1.0 Universal</option>
|
||||
<option value="cecill-2.1">CeCILL Free Software License Agreement v2.1</option>
|
||||
<option value="cern-ohl-p-2.0">CERN Open Hardware Licence Version 2 - Permissive</option>
|
||||
<option value="cern-ohl-s-2.0">CERN Open Hardware Licence Version 2 - Strongly Reciprocal</option>
|
||||
<option value="cern-ohl-w-2.0">CERN Open Hardware Licence Version 2 - Weakly Reciprocal</option>
|
||||
<option value="ecl-2.0">Educational Community License v2.0</option>
|
||||
<option value="epl-1.0">Eclipse Public License 1.0</option>
|
||||
<option value="epl-2.0">Eclipse Public License 2.0</option>
|
||||
<option value="eupl-1.1">European Union Public License 1.1</option>
|
||||
<option value="eupl-1.2">European Union Public License 1.2</option>
|
||||
<option value="gfdl-1.3">GNU Free Documentation License v1.3</option>
|
||||
<option value="gpl-2.0">GNU General Public License v2.0</option>
|
||||
<option value="gpl-3.0">GNU General Public License v3.0</option>
|
||||
<option value="isc">ISC License</option>
|
||||
<option value="lgpl-2.1">GNU Lesser General Public License v2.1</option>
|
||||
<option value="lgpl-3.0">GNU Lesser General Public License v3.0</option>
|
||||
<option value="lppl-1.3c">LaTeX Project Public License v1.3c</option>
|
||||
<option value="mit">MIT License</option>
|
||||
<option value="mit-0">MIT No Attribution</option>
|
||||
<option value="mpl-2.0">Mozilla Public License 2.0</option>
|
||||
<option value="ms-pl">Microsoft Public License</option>
|
||||
<option value="ms-rl">Microsoft Reciprocal License</option>
|
||||
<option value="mulanpsl-2.0">Mulan Permissive Software License, Version 2</option>
|
||||
<option value="ncsa">University of Illinois/NCSA Open Source License</option>
|
||||
<option value="odbl-1.0">Open Data Commons Open Database License v1.0</option>
|
||||
<option value="ofl-1.1">SIL Open Font License 1.1</option>
|
||||
<option value="osl-3.0">Open Software License 3.0</option>
|
||||
<option value="postgresql">PostgreSQL License</option>
|
||||
<option value="unlicense">The Unlicense</option>
|
||||
<option value="upl-1.0">Universal Permissive License v1.0</option>
|
||||
<option value="vim">Vim License</option>
|
||||
<option value="wtfpl">Do What The F*ck You Want To Public License</option>
|
||||
<option value="zlib">zlib License</option>
|
||||
</optgroup>
|
||||
<optgroup label="License families">
|
||||
<option value="cc">Creative Commons</option>
|
||||
<option value="gpl">GNU General Public License</option>
|
||||
<option value="lgpl">GNU Lesser General Public License</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</dd>
|
||||
</dl>
|
||||
<label>
|
||||
Return repositories <select class="form-select js-advanced-search-prefix" data-search-prefix="fork:" data-search-type="Repositories">
|
||||
<option value="">not</option>
|
||||
<option value="true">and</option>
|
||||
<option value="only">only</option>
|
||||
</select> including forks.
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset class="pb-3 mb-4 border-bottom color-border-muted min-width-0">
|
||||
<h3>Code options</h3>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_extension">With this extension</label></dt>
|
||||
<dd>
|
||||
<input id="search_extension" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="rb, py, jpg" data-search-type="Code" data-search-prefix="path:" data-glob-pattern="*.$0" data-regex-pattern="/.$0$/" data-use-or="true">
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_path">In this path</label></dt>
|
||||
<dd><input id="search_path" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="/foo/bar/baz/qux" data-search-prefix="path:" data-search-type="Code" data-use-or=""></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_filename">With this file name</label></dt>
|
||||
<dd>
|
||||
<input id="search_filename" type="text" class="form-control js-advanced-search-prefix" placeholder="app.rb, footer.erb" data-search-type="code:" data-search-prefix="path:" data-glob-pattern="**/$0" data-regex-pattern="/(^|/)$0$/" data-use-or="true">
|
||||
</dd>
|
||||
</dl>
|
||||
<label>
|
||||
Return code <select class="form-select js-advanced-search-prefix" data-search-prefix="fork:" data-search-type="Code">
|
||||
<option value="">not</option>
|
||||
<option value="true">and</option>
|
||||
<option value="only">only</option>
|
||||
</select> including forks.
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset class="pb-3 mb-4 border-bottom color-border-muted min-width-0">
|
||||
<h3>Issues options</h3>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_state">In the state</label></dt>
|
||||
<dd><select id="search_state" class="form-select js-advanced-search-prefix" data-search-prefix="state:" data-search-type="Issues">
|
||||
<option value="">open/closed</option>
|
||||
<option value="open">open</option>
|
||||
<option value="closed">closed</option>
|
||||
</select></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_state_reason">With the reason</label></dt>
|
||||
<dd><select id="search_state_reason" class="form-select js-advanced-search-prefix" data-search-prefix="reason:" data-search-type="Issues">
|
||||
<option value="">any reason</option>
|
||||
<option value="completed">completed</option>
|
||||
<option value="not planned">not planned</option>
|
||||
<option value="reopened">reopened</option>
|
||||
</select></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_comments">With this many comments</label></dt>
|
||||
<dd><input id="search_comments" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="0..100, >442" data-search-prefix="comments:" data-search-type="Issues"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_labels">With the labels</label></dt>
|
||||
<dd><input id="search_labels" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="bug, ie6" data-search-prefix="label:" data-search-type="Issues"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_author">Opened by the author</label></dt>
|
||||
<dd><input id="search_author" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="hubot, octocat" data-search-prefix="author:" data-search-type="Issues"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_mention">Mentioning the users</label></dt>
|
||||
<dd><input id="search_mention" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="tpope, mattt" data-search-prefix="mentions:" data-search-type="Issues"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_assignment">Assigned to the users</label></dt>
|
||||
<dd><input id="search_assignment" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="twp, jim" data-search-prefix="assignee:" data-search-type="Issues"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_updated_date">Updated before the date</label></dt>
|
||||
<dd><input id="search_updated_date" type="text" class="form-control js-advanced-search-prefix" value="" placeholder="<YYYY-MM-DD" data-search-prefix="updated:" data-search-type="Issues"></dd>
|
||||
</dl>
|
||||
</fieldset>
|
||||
<fieldset class="pb-3 mb-4 border-bottom color-border-muted min-width-0">
|
||||
<h3>Users options</h3>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_full_name">With this full name</label></dt>
|
||||
<dd><input id="search_full_name" type="text" class="form-control js-advanced-search-prefix" placeholder="Grace Hopper" data-search-prefix="fullname:" data-search-type="Users"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_location">From this location</label></dt>
|
||||
<dd><input id="search_location" type="text" class="form-control js-advanced-search-prefix" placeholder="San Francisco, CA" data-search-prefix="location:" data-search-type="Users"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_followers">With this many followers</label></dt>
|
||||
<dd><input id="search_followers" type="text" class="form-control js-advanced-search-prefix" placeholder="20..50, >200, <2" data-search-prefix="followers:" data-search-type="Users"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_public_repos">With this many public repositories</label></dt>
|
||||
<dd><input id="search_public_repos" type="text" class="form-control js-advanced-search-prefix" placeholder="0, <42, >5" data-search-prefix="repos:" data-search-type="Users"></dd>
|
||||
</dl>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_user_language">Working in this language</label></dt>
|
||||
<dd>
|
||||
<select id="search_user_language" name="l" class="form-select js-advanced-search-prefix" data-search-prefix="language:">
|
||||
<option value="">Any language</option>
|
||||
<optgroup label="Popular">
|
||||
<option value="C">C</option>
|
||||
<option value="C#">C#</option>
|
||||
<option value="C++">C++</option>
|
||||
<option value="CoffeeScript">CoffeeScript</option>
|
||||
<option value="CSS">CSS</option>
|
||||
<option value="Dart">Dart</option>
|
||||
<option value="DM">DM</option>
|
||||
<option value="Elixir">Elixir</option>
|
||||
<option value="Go">Go</option>
|
||||
<option value="Groovy">Groovy</option>
|
||||
<option value="HTML">HTML</option>
|
||||
<option value="Java">Java</option>
|
||||
<option value="JavaScript">JavaScript</option>
|
||||
<option value="Kotlin">Kotlin</option>
|
||||
<option value="Objective-C">Objective-C</option>
|
||||
<option value="Perl">Perl</option>
|
||||
<option value="PHP">PHP</option>
|
||||
<option value="PowerShell">PowerShell</option>
|
||||
<option value="Python">Python</option>
|
||||
<option value="Ruby">Ruby</option>
|
||||
<option value="Rust">Rust</option>
|
||||
<option value="Scala">Scala</option>
|
||||
<option value="Shell">Shell</option>
|
||||
<option value="Swift">Swift</option>
|
||||
<option value="TypeScript">TypeScript</option>
|
||||
</optgroup>
|
||||
<optgroup label="Everything else">
|
||||
<option value="1C Enterprise">1C Enterprise</option>
|
||||
<option value="2-Dimensional Array">2-Dimensional Array</option>
|
||||
<option value="4D">4D</option>
|
||||
<option value="ABAP">ABAP</option>
|
||||
<option value="ABAP CDS">ABAP CDS</option>
|
||||
<option value="ABNF">ABNF</option>
|
||||
<option value="ActionScript">ActionScript</option>
|
||||
<option value="Ada">Ada</option>
|
||||
|
||||
<option value="Yul">Yul</option>
|
||||
<option value="ZAP">ZAP</option>
|
||||
<option value="Zeek">Zeek</option>
|
||||
<option value="ZenScript">ZenScript</option>
|
||||
<option value="Zephir">Zephir</option>
|
||||
<option value="Zig">Zig</option>
|
||||
<option value="ZIL">ZIL</option>
|
||||
<option value="Zimpl">Zimpl</option>
|
||||
<option value="Zmodel">Zmodel</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</dd>
|
||||
</dl>
|
||||
</fieldset>
|
||||
<fieldset class="pb-3 mb-4 border-bottom color-border-muted min-width-0">
|
||||
<h3>Wiki options</h3>
|
||||
<dl class="form-group flattened d-flex d-md-block flex-column">
|
||||
<dt><label for="search_wiki_updated_date">Updated before the date</label></dt>
|
||||
<dd><input id="search_wiki_updated_date" type="text" class="form-control js-advanced-search-prefix" placeholder="<YYYY-MM-DD" data-search-prefix="updated:" data-search-type="Wiki"></dd>
|
||||
</dl>
|
||||
</fieldset>
|
||||
<div class="form-group flattened">
|
||||
<div class="d-flex d-md-block"> <button type="submit" data-view-component="true" class="btn flex-auto"> Search
|
||||
</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
@@ -1,17 +1,37 @@
|
||||
# C4A-Script Interactive Tutorial
|
||||
|
||||
Welcome to the C4A-Script Interactive Tutorial! This hands-on tutorial teaches you how to write web automation scripts using C4A-Script, a domain-specific language for Crawl4AI.
|
||||
A comprehensive web-based tutorial for learning and experimenting with C4A-Script - Crawl4AI's visual web automation language.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Start the Tutorial Server
|
||||
### Prerequisites
|
||||
- Python 3.7+
|
||||
- Modern web browser (Chrome, Firefox, Safari, Edge)
|
||||
|
||||
```bash
|
||||
cd docs/examples/c4a_script/tutorial
|
||||
python server.py
|
||||
```
|
||||
### Running the Tutorial
|
||||
|
||||
Then open your browser to: http://localhost:8080
|
||||
1. **Clone and Navigate**
|
||||
```bash
|
||||
git clone https://github.com/unclecode/crawl4ai.git
|
||||
cd crawl4ai/docs/examples/c4a_script/tutorial/
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
```bash
|
||||
pip install flask
|
||||
```
|
||||
|
||||
3. **Launch the Server**
|
||||
```bash
|
||||
python server.py
|
||||
```
|
||||
|
||||
4. **Open in Browser**
|
||||
```
|
||||
http://localhost:8080
|
||||
```
|
||||
|
||||
**🌐 Try Online**: [Live Demo](https://docs.crawl4ai.com/c4a-script/demo)
|
||||
|
||||
### 2. Try Your First Script
|
||||
|
||||
@@ -23,7 +43,16 @@ IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
|
||||
CLICK `#start-tutorial`
|
||||
```
|
||||
|
||||
## 📚 What You'll Learn
|
||||
## 🎯 What You'll Learn
|
||||
|
||||
### Core Features
|
||||
- **📝 Text Editor**: Write C4A-Script with syntax highlighting
|
||||
- **🧩 Visual Editor**: Build scripts using drag-and-drop Blockly interface
|
||||
- **🎬 Recording Mode**: Capture browser actions and auto-generate scripts
|
||||
- **⚡ Live Execution**: Run scripts in real-time with instant feedback
|
||||
- **📊 Timeline View**: Visualize and edit automation steps
|
||||
|
||||
## 📚 Tutorial Content
|
||||
|
||||
### Basic Commands
|
||||
- **Navigation**: `GO url`
|
||||
@@ -237,10 +266,131 @@ Check the `scripts/` folder for complete examples:
|
||||
- `04-multi-step-form.c4a` - Complex forms
|
||||
- `05-complex-workflow.c4a` - Full automation
|
||||
|
||||
## 🏗️ Developer Guide
|
||||
|
||||
### Project Architecture
|
||||
|
||||
```
|
||||
tutorial/
|
||||
├── server.py # Flask application server
|
||||
├── assets/ # Tutorial-specific assets
|
||||
│ ├── app.js # Main application logic
|
||||
│ ├── c4a-blocks.js # Custom Blockly blocks
|
||||
│ ├── c4a-generator.js # Code generation
|
||||
│ ├── blockly-manager.js # Blockly integration
|
||||
│ └── styles.css # Main styling
|
||||
├── playground/ # Interactive demo environment
|
||||
│ ├── index.html # Demo web application
|
||||
│ ├── app.js # Demo app logic
|
||||
│ └── styles.css # Demo styling
|
||||
├── scripts/ # Example C4A scripts
|
||||
└── index.html # Main tutorial interface
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
#### 1. TutorialApp (`assets/app.js`)
|
||||
Main application controller managing:
|
||||
- Code editor integration (CodeMirror)
|
||||
- Script execution and browser preview
|
||||
- Tutorial navigation and lessons
|
||||
- State management and persistence
|
||||
|
||||
#### 2. BlocklyManager (`assets/blockly-manager.js`)
|
||||
Visual programming interface:
|
||||
- Custom C4A-Script block definitions
|
||||
- Bidirectional sync between visual blocks and text
|
||||
- Real-time code generation
|
||||
- Dark theme integration
|
||||
|
||||
#### 3. Recording System
|
||||
Powers the recording functionality:
|
||||
- Browser event capture
|
||||
- Smart event grouping and filtering
|
||||
- Automatic C4A-Script generation
|
||||
- Timeline visualization
|
||||
|
||||
### Customization
|
||||
|
||||
#### Adding New Commands
|
||||
1. **Define Block** (`assets/c4a-blocks.js`)
|
||||
2. **Add Generator** (`assets/c4a-generator.js`)
|
||||
3. **Update Parser** (`assets/blockly-manager.js`)
|
||||
|
||||
#### Themes and Styling
|
||||
- Main styles: `assets/styles.css`
|
||||
- Theme variables: CSS custom properties
|
||||
- Dark mode: Auto-applied based on system preference
|
||||
|
||||
### Configuration
|
||||
```python
|
||||
# server.py configuration
|
||||
PORT = 8080
|
||||
DEBUG = True
|
||||
THREADED = True
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
- `GET /` - Main tutorial interface
|
||||
- `GET /playground/` - Interactive demo environment
|
||||
- `POST /execute` - Script execution endpoint
|
||||
- `GET /examples/<script>` - Load example scripts
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Port Already in Use**
|
||||
```bash
|
||||
# Kill existing process
|
||||
lsof -ti:8080 | xargs kill -9
|
||||
# Or use different port
|
||||
python server.py --port 8081
|
||||
```
|
||||
|
||||
**Blockly Not Loading**
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify all static files are served correctly
|
||||
- Ensure proper script loading order
|
||||
|
||||
**Recording Issues**
|
||||
- Verify iframe permissions
|
||||
- Check cross-origin communication
|
||||
- Ensure event listeners are attached
|
||||
|
||||
### Debug Mode
|
||||
Enable detailed logging by setting `DEBUG = True` in `assets/app.js`
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- **[C4A-Script Documentation](../../md_v2/core/c4a-script.md)** - Complete language guide
|
||||
- **[API Reference](../../md_v2/api/c4a-script-reference.md)** - Detailed command documentation
|
||||
- **[Live Demo](https://docs.crawl4ai.com/c4a-script/demo)** - Try without installation
|
||||
- **[Example Scripts](../)** - More automation examples
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Found a bug or have a suggestion? Please open an issue on GitHub!
|
||||
### Bug Reports
|
||||
1. Check existing issues on GitHub
|
||||
2. Provide minimal reproduction steps
|
||||
3. Include browser and system information
|
||||
4. Add relevant console logs
|
||||
|
||||
### Feature Requests
|
||||
1. Fork the repository
|
||||
2. Create feature branch: `git checkout -b feature/my-feature`
|
||||
3. Test thoroughly with different browsers
|
||||
4. Update documentation
|
||||
5. Submit pull request
|
||||
|
||||
### Code Style
|
||||
- Use consistent indentation (2 spaces for JS, 4 for Python)
|
||||
- Add comments for complex logic
|
||||
- Follow existing naming conventions
|
||||
- Test with multiple browsers
|
||||
|
||||
---
|
||||
|
||||
Happy automating with C4A-Script! 🎉
|
||||
**Happy Automating!** 🎉
|
||||
|
||||
Need help? Check our [documentation](https://docs.crawl4ai.com) or open an issue on [GitHub](https://github.com/unclecode/crawl4ai).
|
||||
@@ -664,4 +664,243 @@ body {
|
||||
.output-section {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
Recording Timeline Styles
|
||||
================================================================ */
|
||||
|
||||
.action-btn.record {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
.action-btn.record:hover {
|
||||
background: var(--error-color);
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
.action-btn.record.recording {
|
||||
background: var(--error-color);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.action-btn.record.recording .icon {
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#editor-view,
|
||||
#timeline-view {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.recording-timeline {
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.timeline-header h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.timeline-events {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.timeline-event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-event:hover {
|
||||
border-color: var(--border-hover);
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
.timeline-event.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(15, 187, 170, 0.1);
|
||||
}
|
||||
|
||||
.event-checkbox {
|
||||
margin-right: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-right: 10px;
|
||||
font-family: 'Dank Mono', monospace;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.event-command {
|
||||
flex: 1;
|
||||
font-family: 'Dank Mono', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.event-command .cmd-name {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.event-command .cmd-selector {
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
.event-command .cmd-value {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.event-command .cmd-detail {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.event-edit {
|
||||
margin-left: 10px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.event-edit:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Event Editor Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--modal-overlay);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.event-editor-modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
z-index: 1000;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.event-editor-modal h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Dank Mono', monospace;
|
||||
}
|
||||
|
||||
.editor-field {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.editor-field label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Dank Mono', monospace;
|
||||
}
|
||||
|
||||
.editor-field input,
|
||||
.editor-field select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
border-radius: 4px;
|
||||
font-family: 'Dank Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.editor-field input:focus,
|
||||
.editor-field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Blockly Button */
|
||||
#blockly-btn .icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Hidden State */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -8,6 +8,8 @@ class TutorialApp {
|
||||
this.tutorialMode = false;
|
||||
this.currentStep = 0;
|
||||
this.tutorialSteps = [];
|
||||
this.recordingManager = null;
|
||||
this.blocklyManager = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
@@ -18,6 +20,12 @@ class TutorialApp {
|
||||
this.setupTabs();
|
||||
this.setupTutorial();
|
||||
this.checkFirstVisit();
|
||||
|
||||
// Initialize recording manager
|
||||
this.recordingManager = new RecordingManager(this);
|
||||
|
||||
// Initialize Blockly manager
|
||||
this.blocklyManager = new BlocklyManager(this);
|
||||
}
|
||||
|
||||
setupEditors() {
|
||||
@@ -618,6 +626,858 @@ style.textContent = `
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Recording Manager Class
|
||||
class RecordingManager {
|
||||
constructor(tutorialApp) {
|
||||
this.app = tutorialApp;
|
||||
this.isRecording = false;
|
||||
this.rawEvents = [];
|
||||
this.groupedEvents = [];
|
||||
this.startTime = 0;
|
||||
this.lastEventTime = 0;
|
||||
this.eventInjected = false;
|
||||
this.keyBuffer = [];
|
||||
this.keyBufferTimeout = null;
|
||||
this.scrollAccumulator = { direction: null, amount: 0, startTime: 0 };
|
||||
this.processedEventIndices = new Set(); // Track which raw events have been processed
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupUI();
|
||||
this.setupMessageHandler();
|
||||
}
|
||||
|
||||
setupUI() {
|
||||
// Record button
|
||||
const recordBtn = document.getElementById('record-btn');
|
||||
recordBtn.addEventListener('click', () => this.toggleRecording());
|
||||
|
||||
// Timeline button
|
||||
const timelineBtn = document.getElementById('timeline-btn');
|
||||
timelineBtn?.addEventListener('click', () => this.showTimeline());
|
||||
|
||||
// Back to editor button
|
||||
document.getElementById('back-to-editor')?.addEventListener('click', () => this.hideTimeline());
|
||||
|
||||
// Timeline controls
|
||||
document.getElementById('select-all-events')?.addEventListener('click', () => this.selectAllEvents());
|
||||
document.getElementById('clear-events')?.addEventListener('click', () => this.clearEvents());
|
||||
document.getElementById('generate-script')?.addEventListener('click', () => this.generateScript());
|
||||
}
|
||||
|
||||
setupMessageHandler() {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'c4a-recording-event' && this.isRecording) {
|
||||
this.handleRecordedEvent(event.data.event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleRecording() {
|
||||
const recordBtn = document.getElementById('record-btn');
|
||||
const timelineBtn = document.getElementById('timeline-btn');
|
||||
|
||||
if (!this.isRecording) {
|
||||
// Start recording
|
||||
this.isRecording = true;
|
||||
this.startTime = Date.now();
|
||||
this.lastEventTime = this.startTime;
|
||||
this.rawEvents = [];
|
||||
this.groupedEvents = [];
|
||||
this.processedEventIndices = new Set();
|
||||
this.keyBuffer = [];
|
||||
this.scrollAccumulator = { direction: null, amount: 0, startTime: 0 };
|
||||
|
||||
recordBtn.classList.add('recording');
|
||||
recordBtn.innerHTML = '<span class="icon">⏹</span>Stop';
|
||||
|
||||
// Show timeline immediately when recording starts
|
||||
timelineBtn.classList.remove('hidden');
|
||||
this.showTimeline();
|
||||
|
||||
this.injectEventCapture();
|
||||
this.app.addConsoleMessage('🔴 Recording started...', 'info');
|
||||
} else {
|
||||
// Stop recording
|
||||
this.isRecording = false;
|
||||
recordBtn.classList.remove('recording');
|
||||
recordBtn.innerHTML = '<span class="icon">⏺</span>Record';
|
||||
|
||||
this.removeEventCapture();
|
||||
this.processEvents();
|
||||
|
||||
this.app.addConsoleMessage('⏹️ Recording stopped', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
showTimeline() {
|
||||
const editorView = document.getElementById('editor-view');
|
||||
const timelineView = document.getElementById('timeline-view');
|
||||
|
||||
editorView.classList.add('hidden');
|
||||
timelineView.classList.remove('hidden');
|
||||
|
||||
// Refresh CodeMirror when switching back
|
||||
this.editorNeedsRefresh = true;
|
||||
}
|
||||
|
||||
hideTimeline() {
|
||||
const editorView = document.getElementById('editor-view');
|
||||
const timelineView = document.getElementById('timeline-view');
|
||||
|
||||
timelineView.classList.add('hidden');
|
||||
editorView.classList.remove('hidden');
|
||||
|
||||
// Refresh CodeMirror after switching
|
||||
if (this.editorNeedsRefresh) {
|
||||
setTimeout(() => this.app.editor.refresh(), 100);
|
||||
this.editorNeedsRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
injectEventCapture() {
|
||||
const iframe = document.getElementById('playground-frame');
|
||||
const script = `
|
||||
(function() {
|
||||
if (window.__c4aRecordingActive) return;
|
||||
window.__c4aRecordingActive = true;
|
||||
|
||||
const captureEvent = (type, event) => {
|
||||
const data = {
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
targetTag: event.target.tagName,
|
||||
targetId: event.target.id,
|
||||
targetClass: event.target.className,
|
||||
targetSelector: generateSelector(event.target),
|
||||
targetType: event.target.type // For input elements
|
||||
};
|
||||
|
||||
// Add type-specific data
|
||||
switch(type) {
|
||||
case 'click':
|
||||
case 'dblclick':
|
||||
case 'contextmenu':
|
||||
data.x = event.clientX;
|
||||
data.y = event.clientY;
|
||||
break;
|
||||
case 'keydown':
|
||||
case 'keyup':
|
||||
data.key = event.key;
|
||||
data.code = event.code;
|
||||
data.ctrlKey = event.ctrlKey;
|
||||
data.shiftKey = event.shiftKey;
|
||||
data.altKey = event.altKey;
|
||||
break;
|
||||
case 'input':
|
||||
case 'change':
|
||||
data.value = event.target.value;
|
||||
data.inputType = event.inputType;
|
||||
// For checkboxes and radio buttons, also capture checked state
|
||||
if (event.target.type === 'checkbox' || event.target.type === 'radio') {
|
||||
data.checked = event.target.checked;
|
||||
}
|
||||
// For select elements, capture selected text
|
||||
if (event.target.tagName === 'SELECT') {
|
||||
data.selectedText = event.target.options[event.target.selectedIndex]?.text || '';
|
||||
}
|
||||
break;
|
||||
case 'scroll':
|
||||
case 'wheel':
|
||||
data.scrollTop = window.scrollY;
|
||||
data.scrollLeft = window.scrollX;
|
||||
data.deltaY = event.deltaY || 0;
|
||||
data.deltaX = event.deltaX || 0;
|
||||
break;
|
||||
case 'focus':
|
||||
case 'blur':
|
||||
data.value = event.target.value || '';
|
||||
break;
|
||||
}
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'c4a-recording-event',
|
||||
event: data
|
||||
}, '*');
|
||||
};
|
||||
|
||||
const generateSelector = (element) => {
|
||||
try {
|
||||
if (element.id) return '#' + element.id;
|
||||
|
||||
if (element.className && typeof element.className === 'string') {
|
||||
const classes = element.className.trim().split(/\\s+/);
|
||||
if (classes.length > 0 && classes[0]) {
|
||||
return '.' + classes[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Generate nth-child selector
|
||||
let path = [];
|
||||
let currentElement = element;
|
||||
while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
|
||||
let selector = currentElement.nodeName.toLowerCase();
|
||||
if (currentElement.id) {
|
||||
selector = '#' + currentElement.id;
|
||||
path.unshift(selector);
|
||||
break;
|
||||
} else {
|
||||
let sibling = currentElement;
|
||||
let nth = 1;
|
||||
while (sibling.previousElementSibling) {
|
||||
sibling = sibling.previousElementSibling;
|
||||
if (sibling.nodeName === currentElement.nodeName) nth++;
|
||||
}
|
||||
if (nth > 1) selector += ':nth-child(' + nth + ')';
|
||||
}
|
||||
path.unshift(selector);
|
||||
currentElement = currentElement.parentNode;
|
||||
}
|
||||
return path.join(' > ') || element.nodeName.toLowerCase();
|
||||
} catch (e) {
|
||||
return element.nodeName.toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
// Store event handlers for cleanup
|
||||
window.__c4aEventHandlers = {};
|
||||
|
||||
// Attach event listeners
|
||||
const events = ['click', 'dblclick', 'contextmenu', 'keydown', 'keyup',
|
||||
'input', 'change', 'scroll', 'wheel', 'focus', 'blur'];
|
||||
|
||||
events.forEach(eventType => {
|
||||
const handler = (e) => captureEvent(eventType, e);
|
||||
window.__c4aEventHandlers[eventType] = handler;
|
||||
document.addEventListener(eventType, handler, true);
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
window.__c4aCleanupRecording = () => {
|
||||
events.forEach(eventType => {
|
||||
const handler = window.__c4aEventHandlers[eventType];
|
||||
if (handler) {
|
||||
document.removeEventListener(eventType, handler, true);
|
||||
}
|
||||
});
|
||||
delete window.__c4aRecordingActive;
|
||||
delete window.__c4aCleanupRecording;
|
||||
delete window.__c4aEventHandlers;
|
||||
};
|
||||
})();
|
||||
`;
|
||||
|
||||
const scriptEl = iframe.contentDocument.createElement('script');
|
||||
scriptEl.textContent = script;
|
||||
iframe.contentDocument.body.appendChild(scriptEl);
|
||||
scriptEl.remove();
|
||||
this.eventInjected = true;
|
||||
}
|
||||
|
||||
removeEventCapture() {
|
||||
if (!this.eventInjected) return;
|
||||
|
||||
const iframe = document.getElementById('playground-frame');
|
||||
iframe.contentWindow.eval('if (window.__c4aCleanupRecording) window.__c4aCleanupRecording();');
|
||||
this.eventInjected = false;
|
||||
}
|
||||
|
||||
handleRecordedEvent(event) {
|
||||
const now = Date.now();
|
||||
const timeSinceStart = ((now - this.startTime) / 1000).toFixed(1);
|
||||
|
||||
// Add time since last event
|
||||
event.timeSinceStart = timeSinceStart;
|
||||
event.timeSinceLast = now - this.lastEventTime;
|
||||
this.lastEventTime = now;
|
||||
|
||||
this.rawEvents.push(event);
|
||||
|
||||
// Real-time processing for immediate feedback
|
||||
if (event.type === 'keydown' && this.shouldGroupKeystrokes(event)) {
|
||||
this.keyBuffer.push(event);
|
||||
|
||||
// Clear existing timeout
|
||||
if (this.keyBufferTimeout) {
|
||||
clearTimeout(this.keyBufferTimeout);
|
||||
}
|
||||
|
||||
// Set timeout to flush buffer after 500ms of no typing
|
||||
this.keyBufferTimeout = setTimeout(() => {
|
||||
this.flushKeyBuffer();
|
||||
this.updateTimeline();
|
||||
}, 500);
|
||||
} else {
|
||||
// Handle change events for select, checkbox, radio
|
||||
if (event.type === 'change') {
|
||||
const tagName = event.targetTag?.toLowerCase();
|
||||
|
||||
// Only skip change events for text inputs (they're part of typing)
|
||||
if (tagName === 'input' &&
|
||||
event.targetType !== 'checkbox' &&
|
||||
event.targetType !== 'radio') {
|
||||
return; // Skip text input change events
|
||||
}
|
||||
|
||||
// For select, checkbox, radio - process the change event
|
||||
if (tagName === 'select' ||
|
||||
(tagName === 'input' && (event.targetType === 'checkbox' || event.targetType === 'radio'))) {
|
||||
|
||||
// Flush any pending keystrokes first
|
||||
if (this.keyBuffer.length > 0) {
|
||||
this.flushKeyBuffer();
|
||||
this.updateTimeline();
|
||||
}
|
||||
|
||||
// Create SET command for the value change
|
||||
const command = this.eventToCommand(event, this.rawEvents.length - 1);
|
||||
if (command) {
|
||||
this.groupedEvents.push(command);
|
||||
this.updateTimeline();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip input events - they're part of typing
|
||||
if (event.type === 'input') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear timeout if exists
|
||||
if (this.keyBufferTimeout) {
|
||||
clearTimeout(this.keyBufferTimeout);
|
||||
this.keyBufferTimeout = null;
|
||||
}
|
||||
|
||||
// Flush key buffer only for significant events
|
||||
const shouldFlushBuffer = event.type === 'click' ||
|
||||
event.type === 'dblclick' ||
|
||||
event.type === 'contextmenu' ||
|
||||
event.type === 'scroll' ||
|
||||
event.type === 'wheel';
|
||||
|
||||
const hadKeyBuffer = this.keyBuffer.length > 0;
|
||||
|
||||
if (shouldFlushBuffer && hadKeyBuffer) {
|
||||
this.flushKeyBuffer();
|
||||
this.updateTimeline();
|
||||
}
|
||||
|
||||
// Process this event immediately if it's not a typing-related event
|
||||
if (event.type !== 'keydown' && event.type !== 'keyup' &&
|
||||
event.type !== 'input' && event.type !== 'change' &&
|
||||
event.type !== 'focus' && event.type !== 'blur') {
|
||||
const command = this.eventToCommand(event, this.rawEvents.length - 1);
|
||||
if (command) {
|
||||
// Check if it's a scroll event that should be accumulated
|
||||
if (command.type === 'SCROLL') {
|
||||
// Remove previous scroll events in the same direction
|
||||
this.groupedEvents = this.groupedEvents.filter(e =>
|
||||
!(e.type === 'SCROLL' && e.direction === command.direction &&
|
||||
parseFloat(e.time) > parseFloat(command.time) - 0.5)
|
||||
);
|
||||
}
|
||||
this.groupedEvents.push(command);
|
||||
this.updateTimeline();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldGroupKeystrokes(event) {
|
||||
// Skip if no key
|
||||
if (!event.key) return false;
|
||||
|
||||
// Group printable characters, space, and common typing keys
|
||||
return (
|
||||
event.key.length === 1 || // Single characters
|
||||
event.key === ' ' || // Space
|
||||
event.key === 'Enter' || // Enter key
|
||||
event.key === 'Tab' || // Tab key
|
||||
event.key === 'Backspace' || // Backspace
|
||||
event.key === 'Delete' // Delete
|
||||
);
|
||||
}
|
||||
|
||||
flushKeyBuffer() {
|
||||
if (this.keyBuffer.length === 0) return;
|
||||
|
||||
// Build the text, handling special keys
|
||||
const text = this.keyBuffer.map(e => {
|
||||
switch(e.key) {
|
||||
case ' ': return ' ';
|
||||
case 'Enter': return '\n';
|
||||
case 'Tab': return '\t';
|
||||
case 'Backspace': return ''; // Skip backspace in final text
|
||||
case 'Delete': return ''; // Skip delete in final text
|
||||
default: return e.key;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
// Don't create empty TYPE commands
|
||||
if (text.length === 0) {
|
||||
this.keyBuffer = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const firstEvent = this.keyBuffer[0];
|
||||
const lastEvent = this.keyBuffer[this.keyBuffer.length - 1];
|
||||
|
||||
// Mark all keystroke events as processed
|
||||
this.keyBuffer.forEach(event => {
|
||||
const index = this.rawEvents.indexOf(event);
|
||||
if (index !== -1) {
|
||||
this.processedEventIndices.add(index);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if this should be a SET command instead of TYPE
|
||||
// Look for a click event just before the first keystroke
|
||||
const firstKeystrokeIndex = this.rawEvents.indexOf(firstEvent);
|
||||
let commandType = 'TYPE';
|
||||
|
||||
if (firstKeystrokeIndex > 0) {
|
||||
const prevEvent = this.rawEvents[firstKeystrokeIndex - 1];
|
||||
if (prevEvent && prevEvent.type === 'click' &&
|
||||
prevEvent.targetSelector === firstEvent.targetSelector) {
|
||||
// This looks like a SET pattern
|
||||
commandType = 'SET';
|
||||
// Mark the click event as processed too
|
||||
this.processedEventIndices.add(firstKeystrokeIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we already have a TYPE command for this exact text at this time
|
||||
// This prevents duplicates when the buffer is flushed multiple times
|
||||
const existingCommand = this.groupedEvents.find(cmd =>
|
||||
cmd.type === commandType &&
|
||||
cmd.value === text &&
|
||||
cmd.time === firstEvent.timeSinceStart
|
||||
);
|
||||
|
||||
if (!existingCommand) {
|
||||
this.groupedEvents.push({
|
||||
type: commandType,
|
||||
selector: firstEvent.targetSelector,
|
||||
value: text,
|
||||
time: firstEvent.timeSinceStart,
|
||||
duration: lastEvent.timestamp - firstEvent.timestamp,
|
||||
raw: [...this.keyBuffer] // Make a copy to avoid reference issues
|
||||
});
|
||||
}
|
||||
|
||||
this.keyBuffer = [];
|
||||
}
|
||||
|
||||
processEvents() {
|
||||
// Clear any pending timeouts
|
||||
if (this.keyBufferTimeout) {
|
||||
clearTimeout(this.keyBufferTimeout);
|
||||
this.keyBufferTimeout = null;
|
||||
}
|
||||
|
||||
// Flush any remaining buffers
|
||||
this.flushKeyBuffer();
|
||||
|
||||
// Don't reprocess events that were already grouped during recording
|
||||
// Just apply final optimizations
|
||||
this.optimizeEvents();
|
||||
this.updateTimeline();
|
||||
}
|
||||
|
||||
eventToCommand(event, index) {
|
||||
// Skip already processed events
|
||||
if (this.processedEventIndices.has(index)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip events that should only be processed as grouped commands
|
||||
if (event.type === 'keydown' || event.type === 'keyup' ||
|
||||
event.type === 'input' || event.type === 'focus' || event.type === 'blur') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allow change events for select, checkbox, radio
|
||||
if (event.type === 'change') {
|
||||
const tagName = event.targetTag?.toLowerCase();
|
||||
if (tagName === 'select' ||
|
||||
(tagName === 'input' && (event.targetType === 'checkbox' || event.targetType === 'radio'))) {
|
||||
// Process as SET command
|
||||
} else {
|
||||
return null; // Skip change events for text inputs
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'click':
|
||||
// Check if followed by input focus or change event
|
||||
const nextEvent = this.rawEvents[index + 1];
|
||||
if (nextEvent && nextEvent.targetSelector === event.targetSelector) {
|
||||
if (nextEvent.type === 'focus' || nextEvent.type === 'change') {
|
||||
return null; // Skip, will be handled by SET or change event
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if this is a click on a select element
|
||||
if (event.targetTag?.toLowerCase() === 'select') {
|
||||
// Look ahead for a change event
|
||||
for (let i = index + 1; i < Math.min(index + 5, this.rawEvents.length); i++) {
|
||||
if (this.rawEvents[i].type === 'change' &&
|
||||
this.rawEvents[i].targetSelector === event.targetSelector) {
|
||||
return null; // Skip click, change event will handle it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'CLICK',
|
||||
selector: event.targetSelector,
|
||||
time: event.timeSinceStart
|
||||
};
|
||||
|
||||
case 'dblclick':
|
||||
return {
|
||||
type: 'DOUBLE_CLICK',
|
||||
selector: event.targetSelector,
|
||||
time: event.timeSinceStart
|
||||
};
|
||||
|
||||
case 'contextmenu':
|
||||
return {
|
||||
type: 'RIGHT_CLICK',
|
||||
selector: event.targetSelector,
|
||||
time: event.timeSinceStart
|
||||
};
|
||||
|
||||
case 'scroll':
|
||||
case 'wheel':
|
||||
// Accumulate scroll events
|
||||
if (event.deltaY !== 0) {
|
||||
const direction = event.deltaY > 0 ? 'DOWN' : 'UP';
|
||||
const amount = Math.abs(event.deltaY);
|
||||
|
||||
if (this.scrollAccumulator.direction === direction &&
|
||||
event.timestamp - this.scrollAccumulator.startTime < 500) {
|
||||
this.scrollAccumulator.amount += amount;
|
||||
} else {
|
||||
this.scrollAccumulator = { direction, amount, startTime: event.timestamp };
|
||||
}
|
||||
|
||||
// Return accumulated scroll at end of sequence
|
||||
const nextEvent = this.rawEvents[index + 1];
|
||||
if (!nextEvent || nextEvent.type !== 'scroll' ||
|
||||
nextEvent.timestamp - event.timestamp > 500) {
|
||||
return {
|
||||
type: 'SCROLL',
|
||||
direction: this.scrollAccumulator.direction,
|
||||
amount: Math.round(this.scrollAccumulator.amount),
|
||||
time: event.timeSinceStart
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
// Input events are handled through keystroke grouping
|
||||
case 'input':
|
||||
return null;
|
||||
|
||||
case 'change':
|
||||
// Handle select, checkbox, radio changes
|
||||
const tagName = event.targetTag?.toLowerCase();
|
||||
|
||||
if (tagName === 'select') {
|
||||
return {
|
||||
type: 'SET',
|
||||
selector: event.targetSelector,
|
||||
value: event.value,
|
||||
displayValue: event.selectedText || event.value,
|
||||
time: event.timeSinceStart
|
||||
};
|
||||
} else if (tagName === 'input' && event.targetType === 'checkbox') {
|
||||
return {
|
||||
type: 'SET',
|
||||
selector: event.targetSelector,
|
||||
value: event.checked ? 'checked' : 'unchecked',
|
||||
time: event.timeSinceStart
|
||||
};
|
||||
} else if (tagName === 'input' && event.targetType === 'radio') {
|
||||
return {
|
||||
type: 'SET',
|
||||
selector: event.targetSelector,
|
||||
value: 'checked',
|
||||
time: event.timeSinceStart
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
optimizeEvents() {
|
||||
const optimized = [];
|
||||
let lastTime = 0;
|
||||
|
||||
this.groupedEvents.forEach((event, index) => {
|
||||
// Insert WAIT if pause > 1 second
|
||||
const currentTime = parseFloat(event.time);
|
||||
if (currentTime - lastTime > 1) {
|
||||
optimized.push({
|
||||
type: 'WAIT',
|
||||
value: Math.round(currentTime - lastTime),
|
||||
time: lastTime.toFixed(1)
|
||||
});
|
||||
}
|
||||
|
||||
optimized.push(event);
|
||||
lastTime = currentTime;
|
||||
});
|
||||
|
||||
this.groupedEvents = optimized;
|
||||
}
|
||||
|
||||
updateTimeline() {
|
||||
const timeline = document.getElementById('timeline-events');
|
||||
timeline.innerHTML = '';
|
||||
|
||||
this.groupedEvents.forEach((event, index) => {
|
||||
const eventEl = this.createEventElement(event, index);
|
||||
timeline.appendChild(eventEl);
|
||||
});
|
||||
}
|
||||
|
||||
createEventElement(event, index) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'timeline-event';
|
||||
div.dataset.index = index;
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'event-checkbox';
|
||||
checkbox.checked = true;
|
||||
checkbox.addEventListener('change', () => {
|
||||
div.classList.toggle('selected', checkbox.checked);
|
||||
});
|
||||
|
||||
const time = document.createElement('span');
|
||||
time.className = 'event-time';
|
||||
time.textContent = event.time + 's';
|
||||
|
||||
const command = document.createElement('span');
|
||||
command.className = 'event-command';
|
||||
command.innerHTML = this.formatCommand(event);
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'event-edit';
|
||||
editBtn.textContent = 'Edit';
|
||||
editBtn.addEventListener('click', () => this.editEvent(index));
|
||||
|
||||
div.appendChild(checkbox);
|
||||
div.appendChild(time);
|
||||
div.appendChild(command);
|
||||
div.appendChild(editBtn);
|
||||
|
||||
// Initially selected
|
||||
div.classList.add('selected');
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
formatCommand(event) {
|
||||
switch (event.type) {
|
||||
case 'CLICK':
|
||||
return `<span class="cmd-name">CLICK</span> <span class="cmd-selector">\`${event.selector}\`</span>`;
|
||||
case 'DOUBLE_CLICK':
|
||||
return `<span class="cmd-name">DOUBLE_CLICK</span> <span class="cmd-selector">\`${event.selector}\`</span>`;
|
||||
case 'RIGHT_CLICK':
|
||||
return `<span class="cmd-name">RIGHT_CLICK</span> <span class="cmd-selector">\`${event.selector}\`</span>`;
|
||||
case 'TYPE':
|
||||
return `<span class="cmd-name">TYPE</span> <span class="cmd-value">"${event.value}"</span> <span class="cmd-detail">(${event.value.length} chars)</span>`;
|
||||
case 'SET':
|
||||
// Use displayValue if available (for select elements)
|
||||
const displayText = event.displayValue || event.value;
|
||||
return `<span class="cmd-name">SET</span> <span class="cmd-selector">\`${event.selector}\`</span> <span class="cmd-value">"${displayText}"</span>`;
|
||||
case 'SCROLL':
|
||||
return `<span class="cmd-name">SCROLL</span> <span class="cmd-value">${event.direction} ${event.amount}</span>`;
|
||||
case 'WAIT':
|
||||
return `<span class="cmd-name">WAIT</span> <span class="cmd-value">${event.value}</span>`;
|
||||
default:
|
||||
return `<span class="cmd-name">${event.type}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
editEvent(index) {
|
||||
const event = this.groupedEvents[index];
|
||||
this.currentEditIndex = index;
|
||||
|
||||
// Show modal
|
||||
const overlay = document.getElementById('event-editor-overlay');
|
||||
const modal = document.getElementById('event-editor-modal');
|
||||
overlay.classList.remove('hidden');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Populate fields
|
||||
document.getElementById('edit-command-type').value = event.type;
|
||||
|
||||
// Show/hide fields based on command type
|
||||
const selectorField = document.getElementById('edit-selector-field');
|
||||
const valueField = document.getElementById('edit-value-field');
|
||||
const directionField = document.getElementById('edit-direction-field');
|
||||
|
||||
selectorField.classList.add('hidden');
|
||||
valueField.classList.add('hidden');
|
||||
directionField.classList.add('hidden');
|
||||
|
||||
switch (event.type) {
|
||||
case 'CLICK':
|
||||
case 'DOUBLE_CLICK':
|
||||
case 'RIGHT_CLICK':
|
||||
selectorField.classList.remove('hidden');
|
||||
document.getElementById('edit-selector').value = event.selector;
|
||||
break;
|
||||
case 'TYPE':
|
||||
valueField.classList.remove('hidden');
|
||||
document.getElementById('edit-value').value = event.value;
|
||||
break;
|
||||
case 'SET':
|
||||
selectorField.classList.remove('hidden');
|
||||
valueField.classList.remove('hidden');
|
||||
document.getElementById('edit-selector').value = event.selector;
|
||||
document.getElementById('edit-value').value = event.value;
|
||||
break;
|
||||
case 'SCROLL':
|
||||
directionField.classList.remove('hidden');
|
||||
valueField.classList.remove('hidden');
|
||||
document.getElementById('edit-direction').value = event.direction;
|
||||
document.getElementById('edit-value').value = event.amount;
|
||||
break;
|
||||
case 'WAIT':
|
||||
valueField.classList.remove('hidden');
|
||||
document.getElementById('edit-value').value = event.value;
|
||||
break;
|
||||
}
|
||||
|
||||
// Setup event handlers
|
||||
this.setupEditModalHandlers();
|
||||
}
|
||||
|
||||
setupEditModalHandlers() {
|
||||
const overlay = document.getElementById('event-editor-overlay');
|
||||
const modal = document.getElementById('event-editor-modal');
|
||||
const cancelBtn = document.getElementById('edit-cancel');
|
||||
const saveBtn = document.getElementById('edit-save');
|
||||
|
||||
const closeModal = () => {
|
||||
overlay.classList.add('hidden');
|
||||
modal.classList.add('hidden');
|
||||
};
|
||||
|
||||
const saveHandler = () => {
|
||||
const event = this.groupedEvents[this.currentEditIndex];
|
||||
|
||||
// Update event based on type
|
||||
switch (event.type) {
|
||||
case 'CLICK':
|
||||
case 'DOUBLE_CLICK':
|
||||
case 'RIGHT_CLICK':
|
||||
event.selector = document.getElementById('edit-selector').value;
|
||||
break;
|
||||
case 'TYPE':
|
||||
event.value = document.getElementById('edit-value').value;
|
||||
break;
|
||||
case 'SET':
|
||||
event.selector = document.getElementById('edit-selector').value;
|
||||
event.value = document.getElementById('edit-value').value;
|
||||
break;
|
||||
case 'SCROLL':
|
||||
event.direction = document.getElementById('edit-direction').value;
|
||||
event.amount = parseInt(document.getElementById('edit-value').value) || 0;
|
||||
break;
|
||||
case 'WAIT':
|
||||
event.value = parseInt(document.getElementById('edit-value').value) || 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Update timeline
|
||||
this.updateTimeline();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
// Clean up old handlers
|
||||
cancelBtn.replaceWith(cancelBtn.cloneNode(true));
|
||||
saveBtn.replaceWith(saveBtn.cloneNode(true));
|
||||
overlay.replaceWith(overlay.cloneNode(true));
|
||||
|
||||
// Add new handlers
|
||||
document.getElementById('edit-cancel').addEventListener('click', closeModal);
|
||||
document.getElementById('edit-save').addEventListener('click', saveHandler);
|
||||
document.getElementById('event-editor-overlay').addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
selectAllEvents() {
|
||||
const checkboxes = document.querySelectorAll('.event-checkbox');
|
||||
const events = document.querySelectorAll('.timeline-event');
|
||||
checkboxes.forEach((cb, i) => {
|
||||
cb.checked = true;
|
||||
events[i].classList.add('selected');
|
||||
});
|
||||
}
|
||||
|
||||
clearEvents() {
|
||||
this.groupedEvents = [];
|
||||
this.updateTimeline();
|
||||
}
|
||||
|
||||
generateScript() {
|
||||
const selectedEvents = [];
|
||||
const checkboxes = document.querySelectorAll('.event-checkbox');
|
||||
|
||||
checkboxes.forEach((cb, index) => {
|
||||
if (cb.checked && this.groupedEvents[index]) {
|
||||
selectedEvents.push(this.groupedEvents[index]);
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedEvents.length === 0) {
|
||||
this.app.addConsoleMessage('No events selected', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const script = selectedEvents.map(event => this.eventToC4A(event)).join('\n');
|
||||
|
||||
// Set the script in the editor
|
||||
this.app.editor.setValue(script);
|
||||
this.app.addConsoleMessage(`Generated ${selectedEvents.length} commands`, 'success');
|
||||
|
||||
// Switch back to editor view
|
||||
this.hideTimeline();
|
||||
}
|
||||
|
||||
eventToC4A(event) {
|
||||
switch (event.type) {
|
||||
case 'CLICK':
|
||||
return `CLICK \`${event.selector}\``;
|
||||
case 'DOUBLE_CLICK':
|
||||
return `DOUBLE_CLICK \`${event.selector}\``;
|
||||
case 'RIGHT_CLICK':
|
||||
return `RIGHT_CLICK \`${event.selector}\``;
|
||||
case 'TYPE':
|
||||
return `TYPE "${event.value}"`;
|
||||
case 'SET':
|
||||
return `SET \`${event.selector}\` "${event.value}"`;
|
||||
case 'SCROLL':
|
||||
return `SCROLL ${event.direction} ${event.amount}`;
|
||||
case 'WAIT':
|
||||
return `WAIT ${event.value}`;
|
||||
default:
|
||||
return `# Unknown: ${event.type}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.tutorialApp = new TutorialApp();
|
||||
|
||||
591
docs/examples/c4a_script/tutorial/assets/blockly-manager.js
Normal file
591
docs/examples/c4a_script/tutorial/assets/blockly-manager.js
Normal file
@@ -0,0 +1,591 @@
|
||||
// Blockly Manager for C4A-Script
|
||||
// Handles Blockly workspace, code generation, and synchronization with text editor
|
||||
|
||||
class BlocklyManager {
|
||||
constructor(tutorialApp) {
|
||||
this.app = tutorialApp;
|
||||
this.workspace = null;
|
||||
this.isUpdating = false; // Prevent circular updates
|
||||
this.blocklyVisible = false;
|
||||
this.toolboxXml = this.generateToolbox();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupBlocklyContainer();
|
||||
this.initializeWorkspace();
|
||||
this.setupEventHandlers();
|
||||
this.setupSynchronization();
|
||||
}
|
||||
|
||||
setupBlocklyContainer() {
|
||||
// Create blockly container div
|
||||
const editorContainer = document.querySelector('.editor-container');
|
||||
const blocklyDiv = document.createElement('div');
|
||||
blocklyDiv.id = 'blockly-view';
|
||||
blocklyDiv.className = 'blockly-workspace hidden';
|
||||
blocklyDiv.style.height = '100%';
|
||||
blocklyDiv.style.width = '100%';
|
||||
editorContainer.appendChild(blocklyDiv);
|
||||
}
|
||||
|
||||
generateToolbox() {
|
||||
return `
|
||||
<xml id="toolbox" style="display: none">
|
||||
<category name="Navigation" colour="${BlockColors.NAVIGATION}">
|
||||
<block type="c4a_go"></block>
|
||||
<block type="c4a_reload"></block>
|
||||
<block type="c4a_back"></block>
|
||||
<block type="c4a_forward"></block>
|
||||
</category>
|
||||
|
||||
<category name="Wait" colour="${BlockColors.WAIT}">
|
||||
<block type="c4a_wait_time">
|
||||
<field name="SECONDS">3</field>
|
||||
</block>
|
||||
<block type="c4a_wait_selector">
|
||||
<field name="SELECTOR">#content</field>
|
||||
<field name="TIMEOUT">10</field>
|
||||
</block>
|
||||
<block type="c4a_wait_text">
|
||||
<field name="TEXT">Loading complete</field>
|
||||
<field name="TIMEOUT">5</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Mouse Actions" colour="${BlockColors.ACTIONS}">
|
||||
<block type="c4a_click">
|
||||
<field name="SELECTOR">button.submit</field>
|
||||
</block>
|
||||
<block type="c4a_click_xy"></block>
|
||||
<block type="c4a_double_click"></block>
|
||||
<block type="c4a_right_click"></block>
|
||||
<block type="c4a_move"></block>
|
||||
<block type="c4a_drag"></block>
|
||||
<block type="c4a_scroll">
|
||||
<field name="DIRECTION">DOWN</field>
|
||||
<field name="AMOUNT">500</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Keyboard" colour="${BlockColors.KEYBOARD}">
|
||||
<block type="c4a_type">
|
||||
<field name="TEXT">hello@example.com</field>
|
||||
</block>
|
||||
<block type="c4a_type_var">
|
||||
<field name="VAR">email</field>
|
||||
</block>
|
||||
<block type="c4a_clear"></block>
|
||||
<block type="c4a_set">
|
||||
<field name="SELECTOR">#email</field>
|
||||
<field name="VALUE">user@example.com</field>
|
||||
</block>
|
||||
<block type="c4a_press">
|
||||
<field name="KEY">Tab</field>
|
||||
</block>
|
||||
<block type="c4a_key_down">
|
||||
<field name="KEY">Shift</field>
|
||||
</block>
|
||||
<block type="c4a_key_up">
|
||||
<field name="KEY">Shift</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Control Flow" colour="${BlockColors.CONTROL}">
|
||||
<block type="c4a_if_exists">
|
||||
<field name="SELECTOR">.cookie-banner</field>
|
||||
</block>
|
||||
<block type="c4a_if_exists_else">
|
||||
<field name="SELECTOR">#user</field>
|
||||
</block>
|
||||
<block type="c4a_if_not_exists">
|
||||
<field name="SELECTOR">.modal</field>
|
||||
</block>
|
||||
<block type="c4a_if_js">
|
||||
<field name="CONDITION">window.innerWidth < 768</field>
|
||||
</block>
|
||||
<block type="c4a_repeat_times">
|
||||
<field name="TIMES">5</field>
|
||||
</block>
|
||||
<block type="c4a_repeat_while">
|
||||
<field name="CONDITION">document.querySelector('.load-more')</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Variables" colour="${BlockColors.VARIABLES}">
|
||||
<block type="c4a_setvar">
|
||||
<field name="NAME">username</field>
|
||||
<field name="VALUE">john@example.com</field>
|
||||
</block>
|
||||
<block type="c4a_eval">
|
||||
<field name="CODE">console.log('Hello')</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Procedures" colour="${BlockColors.PROCEDURES}">
|
||||
<block type="c4a_proc_def">
|
||||
<field name="NAME">login</field>
|
||||
</block>
|
||||
<block type="c4a_proc_call">
|
||||
<field name="NAME">login</field>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Comments" colour="#9E9E9E">
|
||||
<block type="c4a_comment">
|
||||
<field name="TEXT">Add comment here</field>
|
||||
</block>
|
||||
</category>
|
||||
</xml>`;
|
||||
}
|
||||
|
||||
initializeWorkspace() {
|
||||
const blocklyDiv = document.getElementById('blockly-view');
|
||||
|
||||
// Dark theme configuration
|
||||
const theme = Blockly.Theme.defineTheme('c4a-dark', {
|
||||
'base': Blockly.Themes.Classic,
|
||||
'componentStyles': {
|
||||
'workspaceBackgroundColour': '#0e0e10',
|
||||
'toolboxBackgroundColour': '#1a1a1b',
|
||||
'toolboxForegroundColour': '#e0e0e0',
|
||||
'flyoutBackgroundColour': '#1a1a1b',
|
||||
'flyoutForegroundColour': '#e0e0e0',
|
||||
'flyoutOpacity': 0.9,
|
||||
'scrollbarColour': '#2a2a2c',
|
||||
'scrollbarOpacity': 0.5,
|
||||
'insertionMarkerColour': '#0fbbaa',
|
||||
'insertionMarkerOpacity': 0.3,
|
||||
'markerColour': '#0fbbaa',
|
||||
'cursorColour': '#0fbbaa',
|
||||
'selectedGlowColour': '#0fbbaa',
|
||||
'selectedGlowOpacity': 0.4,
|
||||
'replacementGlowColour': '#0fbbaa',
|
||||
'replacementGlowOpacity': 0.5
|
||||
},
|
||||
'fontStyle': {
|
||||
'family': 'Dank Mono, Monaco, Consolas, monospace',
|
||||
'weight': 'normal',
|
||||
'size': 13
|
||||
}
|
||||
});
|
||||
|
||||
this.workspace = Blockly.inject(blocklyDiv, {
|
||||
toolbox: this.toolboxXml,
|
||||
theme: theme,
|
||||
grid: {
|
||||
spacing: 20,
|
||||
length: 3,
|
||||
colour: '#2a2a2c',
|
||||
snap: true
|
||||
},
|
||||
zoom: {
|
||||
controls: true,
|
||||
wheel: true,
|
||||
startScale: 1.0,
|
||||
maxScale: 3,
|
||||
minScale: 0.3,
|
||||
scaleSpeed: 1.2
|
||||
},
|
||||
trashcan: true,
|
||||
sounds: false,
|
||||
media: 'https://unpkg.com/blockly/media/'
|
||||
});
|
||||
|
||||
// Add workspace change listener
|
||||
this.workspace.addChangeListener((event) => {
|
||||
if (!this.isUpdating && event.type !== Blockly.Events.UI) {
|
||||
this.syncBlocksToCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupEventHandlers() {
|
||||
// Add blockly toggle button
|
||||
const headerActions = document.querySelector('.editor-panel .header-actions');
|
||||
const blocklyBtn = document.createElement('button');
|
||||
blocklyBtn.id = 'blockly-btn';
|
||||
blocklyBtn.className = 'action-btn';
|
||||
blocklyBtn.title = 'Toggle Blockly Mode';
|
||||
blocklyBtn.innerHTML = '<span class="icon">🧩</span>';
|
||||
|
||||
// Insert before the Run button
|
||||
const runBtn = document.getElementById('run-btn');
|
||||
headerActions.insertBefore(blocklyBtn, runBtn);
|
||||
|
||||
blocklyBtn.addEventListener('click', () => this.toggleBlocklyView());
|
||||
}
|
||||
|
||||
setupSynchronization() {
|
||||
// Listen to CodeMirror changes
|
||||
this.app.editor.on('change', (instance, changeObj) => {
|
||||
if (!this.isUpdating && this.blocklyVisible && changeObj.origin !== 'setValue') {
|
||||
this.syncCodeToBlocks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleBlocklyView() {
|
||||
const editorView = document.getElementById('editor-view');
|
||||
const blocklyView = document.getElementById('blockly-view');
|
||||
const timelineView = document.getElementById('timeline-view');
|
||||
const blocklyBtn = document.getElementById('blockly-btn');
|
||||
|
||||
this.blocklyVisible = !this.blocklyVisible;
|
||||
|
||||
if (this.blocklyVisible) {
|
||||
// Show Blockly
|
||||
editorView.classList.add('hidden');
|
||||
timelineView.classList.add('hidden');
|
||||
blocklyView.classList.remove('hidden');
|
||||
blocklyBtn.classList.add('active');
|
||||
|
||||
// Resize workspace
|
||||
Blockly.svgResize(this.workspace);
|
||||
|
||||
// Sync current code to blocks
|
||||
this.syncCodeToBlocks();
|
||||
} else {
|
||||
// Show editor
|
||||
blocklyView.classList.add('hidden');
|
||||
editorView.classList.remove('hidden');
|
||||
blocklyBtn.classList.remove('active');
|
||||
|
||||
// Refresh CodeMirror
|
||||
setTimeout(() => this.app.editor.refresh(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
syncBlocksToCode() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
try {
|
||||
this.isUpdating = true;
|
||||
|
||||
// Generate C4A-Script from blocks using our custom generator
|
||||
if (typeof c4aGenerator !== 'undefined') {
|
||||
const code = c4aGenerator.workspaceToCode(this.workspace);
|
||||
|
||||
// Process the code to maintain proper formatting
|
||||
const lines = code.split('\n');
|
||||
const formattedLines = [];
|
||||
let lastWasComment = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
const isComment = line.startsWith('#');
|
||||
|
||||
// Add blank line when transitioning between comments and commands
|
||||
if (formattedLines.length > 0 && lastWasComment !== isComment) {
|
||||
formattedLines.push('');
|
||||
}
|
||||
|
||||
formattedLines.push(line);
|
||||
lastWasComment = isComment;
|
||||
}
|
||||
|
||||
const cleanCode = formattedLines.join('\n');
|
||||
|
||||
// Update CodeMirror
|
||||
this.app.editor.setValue(cleanCode);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error syncing blocks to code:', error);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
syncCodeToBlocks() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
try {
|
||||
this.isUpdating = true;
|
||||
|
||||
// Clear workspace
|
||||
this.workspace.clear();
|
||||
|
||||
// Parse C4A-Script and generate blocks
|
||||
const code = this.app.editor.getValue();
|
||||
const blocks = this.parseC4AToBlocks(code);
|
||||
|
||||
if (blocks) {
|
||||
Blockly.Xml.domToWorkspace(blocks, this.workspace);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error syncing code to blocks:', error);
|
||||
// Show error in console
|
||||
this.app.addConsoleMessage(`Blockly sync error: ${error.message}`, 'warning');
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
parseC4AToBlocks(code) {
|
||||
const lines = code.split('\n');
|
||||
const xml = document.createElement('xml');
|
||||
let yPos = 20;
|
||||
let previousBlock = null;
|
||||
let rootBlock = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// Skip empty lines
|
||||
if (!line) continue;
|
||||
|
||||
// Handle comments
|
||||
if (line.startsWith('#')) {
|
||||
const commentBlock = this.parseLineToBlock(line, i, lines);
|
||||
if (commentBlock) {
|
||||
if (previousBlock) {
|
||||
// Connect to previous block
|
||||
const next = document.createElement('next');
|
||||
next.appendChild(commentBlock);
|
||||
previousBlock.appendChild(next);
|
||||
} else {
|
||||
// First block - set position
|
||||
commentBlock.setAttribute('x', 20);
|
||||
commentBlock.setAttribute('y', yPos);
|
||||
xml.appendChild(commentBlock);
|
||||
rootBlock = commentBlock;
|
||||
yPos += 60;
|
||||
}
|
||||
previousBlock = commentBlock;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const block = this.parseLineToBlock(line, i, lines);
|
||||
|
||||
if (block) {
|
||||
if (previousBlock) {
|
||||
// Connect to previous block using <next>
|
||||
const next = document.createElement('next');
|
||||
next.appendChild(block);
|
||||
previousBlock.appendChild(next);
|
||||
} else {
|
||||
// First block - set position
|
||||
block.setAttribute('x', 20);
|
||||
block.setAttribute('y', yPos);
|
||||
xml.appendChild(block);
|
||||
rootBlock = block;
|
||||
yPos += 60;
|
||||
}
|
||||
previousBlock = block;
|
||||
}
|
||||
}
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
parseLineToBlock(line, index, allLines) {
|
||||
// Navigation commands
|
||||
if (line.startsWith('GO ')) {
|
||||
const url = line.substring(3).trim();
|
||||
return this.createBlock('c4a_go', { 'URL': url });
|
||||
}
|
||||
if (line === 'RELOAD') {
|
||||
return this.createBlock('c4a_reload');
|
||||
}
|
||||
if (line === 'BACK') {
|
||||
return this.createBlock('c4a_back');
|
||||
}
|
||||
if (line === 'FORWARD') {
|
||||
return this.createBlock('c4a_forward');
|
||||
}
|
||||
|
||||
// Wait commands
|
||||
if (line.startsWith('WAIT ')) {
|
||||
const parts = line.substring(5).trim();
|
||||
|
||||
// Check if it's just a number (wait time)
|
||||
if (/^\d+(\.\d+)?$/.test(parts)) {
|
||||
return this.createBlock('c4a_wait_time', { 'SECONDS': parts });
|
||||
}
|
||||
|
||||
// Check for selector wait
|
||||
const selectorMatch = parts.match(/^`([^`]+)`\s+(\d+)$/);
|
||||
if (selectorMatch) {
|
||||
return this.createBlock('c4a_wait_selector', {
|
||||
'SELECTOR': selectorMatch[1],
|
||||
'TIMEOUT': selectorMatch[2]
|
||||
});
|
||||
}
|
||||
|
||||
// Check for text wait
|
||||
const textMatch = parts.match(/^"([^"]+)"\s+(\d+)$/);
|
||||
if (textMatch) {
|
||||
return this.createBlock('c4a_wait_text', {
|
||||
'TEXT': textMatch[1],
|
||||
'TIMEOUT': textMatch[2]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Click commands
|
||||
if (line.startsWith('CLICK ')) {
|
||||
const target = line.substring(6).trim();
|
||||
|
||||
// Check for coordinates
|
||||
const coordMatch = target.match(/^(\d+)\s+(\d+)$/);
|
||||
if (coordMatch) {
|
||||
return this.createBlock('c4a_click_xy', {
|
||||
'X': coordMatch[1],
|
||||
'Y': coordMatch[2]
|
||||
});
|
||||
}
|
||||
|
||||
// Selector click
|
||||
const selectorMatch = target.match(/^`([^`]+)`$/);
|
||||
if (selectorMatch) {
|
||||
return this.createBlock('c4a_click', {
|
||||
'SELECTOR': selectorMatch[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Other mouse actions
|
||||
if (line.startsWith('DOUBLE_CLICK ')) {
|
||||
const selector = line.substring(13).trim().match(/^`([^`]+)`$/);
|
||||
if (selector) {
|
||||
return this.createBlock('c4a_double_click', {
|
||||
'SELECTOR': selector[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (line.startsWith('RIGHT_CLICK ')) {
|
||||
const selector = line.substring(12).trim().match(/^`([^`]+)`$/);
|
||||
if (selector) {
|
||||
return this.createBlock('c4a_right_click', {
|
||||
'SELECTOR': selector[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll
|
||||
if (line.startsWith('SCROLL ')) {
|
||||
const match = line.match(/^SCROLL\s+(UP|DOWN|LEFT|RIGHT)(?:\s+(\d+))?$/);
|
||||
if (match) {
|
||||
return this.createBlock('c4a_scroll', {
|
||||
'DIRECTION': match[1],
|
||||
'AMOUNT': match[2] || '500'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Type commands
|
||||
if (line.startsWith('TYPE ')) {
|
||||
const content = line.substring(5).trim();
|
||||
|
||||
// Variable type
|
||||
if (content.startsWith('$')) {
|
||||
return this.createBlock('c4a_type_var', {
|
||||
'VAR': content.substring(1)
|
||||
});
|
||||
}
|
||||
|
||||
// Text type
|
||||
const textMatch = content.match(/^"([^"]*)"$/);
|
||||
if (textMatch) {
|
||||
return this.createBlock('c4a_type', {
|
||||
'TEXT': textMatch[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// SET command
|
||||
if (line.startsWith('SET ')) {
|
||||
const match = line.match(/^SET\s+`([^`]+)`\s+"([^"]*)"$/);
|
||||
if (match) {
|
||||
return this.createBlock('c4a_set', {
|
||||
'SELECTOR': match[1],
|
||||
'VALUE': match[2]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// CLEAR command
|
||||
if (line.startsWith('CLEAR ')) {
|
||||
const match = line.match(/^CLEAR\s+`([^`]+)`$/);
|
||||
if (match) {
|
||||
return this.createBlock('c4a_clear', {
|
||||
'SELECTOR': match[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// SETVAR command
|
||||
if (line.startsWith('SETVAR ')) {
|
||||
const match = line.match(/^SETVAR\s+(\w+)\s*=\s*"([^"]*)"$/);
|
||||
if (match) {
|
||||
return this.createBlock('c4a_setvar', {
|
||||
'NAME': match[1],
|
||||
'VALUE': match[2]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// IF commands (simplified - only single line)
|
||||
if (line.startsWith('IF ')) {
|
||||
// IF EXISTS
|
||||
const existsMatch = line.match(/^IF\s+\(EXISTS\s+`([^`]+)`\)\s+THEN\s+(.+?)(?:\s+ELSE\s+(.+))?$/);
|
||||
if (existsMatch) {
|
||||
if (existsMatch[3]) {
|
||||
// Has ELSE
|
||||
const block = this.createBlock('c4a_if_exists_else', {
|
||||
'SELECTOR': existsMatch[1]
|
||||
});
|
||||
// Parse then and else commands - simplified for now
|
||||
return block;
|
||||
} else {
|
||||
// No ELSE
|
||||
const block = this.createBlock('c4a_if_exists', {
|
||||
'SELECTOR': existsMatch[1]
|
||||
});
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
// IF NOT EXISTS
|
||||
const notExistsMatch = line.match(/^IF\s+\(NOT\s+EXISTS\s+`([^`]+)`\)\s+THEN\s+(.+)$/);
|
||||
if (notExistsMatch) {
|
||||
const block = this.createBlock('c4a_if_not_exists', {
|
||||
'SELECTOR': notExistsMatch[1]
|
||||
});
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
// Comments
|
||||
if (line.startsWith('#')) {
|
||||
return this.createBlock('c4a_comment', {
|
||||
'TEXT': line.substring(1).trim()
|
||||
});
|
||||
}
|
||||
|
||||
// If we can't parse it, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
createBlock(type, fields = {}) {
|
||||
const block = document.createElement('block');
|
||||
block.setAttribute('type', type);
|
||||
|
||||
// Add fields
|
||||
for (const [name, value] of Object.entries(fields)) {
|
||||
const field = document.createElement('field');
|
||||
field.setAttribute('name', name);
|
||||
field.textContent = value;
|
||||
block.appendChild(field);
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
}
|
||||
238
docs/examples/c4a_script/tutorial/assets/blockly-theme.css
Normal file
238
docs/examples/c4a_script/tutorial/assets/blockly-theme.css
Normal file
@@ -0,0 +1,238 @@
|
||||
/* Blockly Theme CSS for C4A-Script */
|
||||
|
||||
/* Blockly workspace container */
|
||||
.blockly-workspace {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* Blockly button active state */
|
||||
#blockly-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: var(--bg-primary);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
#blockly-btn.active:hover {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* Override Blockly's default styles for dark theme */
|
||||
.blocklyToolboxDiv {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border-right: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
.blocklyFlyout {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
.blocklyFlyoutBackground {
|
||||
fill: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
.blocklyMainBackground {
|
||||
stroke: none !important;
|
||||
}
|
||||
|
||||
.blocklyTreeRow {
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
padding: 4px 16px !important;
|
||||
margin: 2px 0 !important;
|
||||
}
|
||||
|
||||
.blocklyTreeRow:hover {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
.blocklyTreeSelected {
|
||||
background-color: var(--primary-dim) !important;
|
||||
}
|
||||
|
||||
.blocklyTreeLabel {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Blockly scrollbars */
|
||||
.blocklyScrollbarHorizontal,
|
||||
.blocklyScrollbarVertical {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHandle {
|
||||
fill: var(--border-color) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHandle:hover {
|
||||
fill: var(--border-hover) !important;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
/* Blockly zoom controls */
|
||||
.blocklyZoom > image {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.blocklyZoom > image:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Blockly trash can */
|
||||
.blocklyTrash {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.blocklyTrash:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Blockly context menus */
|
||||
.blocklyContextMenu {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.blocklyMenuItem {
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
}
|
||||
|
||||
.blocklyMenuItemDisabled {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.blocklyMenuItem:hover {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
/* Blockly text inputs */
|
||||
.blocklyHtmlInput {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
font-size: 13px !important;
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
.blocklyHtmlInput:focus {
|
||||
border-color: var(--primary-color) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Blockly dropdowns */
|
||||
.blocklyDropDownDiv {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.blocklyDropDownContent {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv .goog-menuitem {
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
padding: 4px 16px !important;
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv .goog-menuitem-highlight,
|
||||
.blocklyDropDownDiv .goog-menuitem-hover {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
/* Custom block colors are defined in the block definitions */
|
||||
|
||||
/* Block text styling */
|
||||
.blocklyText {
|
||||
fill: #ffffff !important;
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.blocklyEditableText > .blocklyText {
|
||||
fill: #ffffff !important;
|
||||
}
|
||||
|
||||
.blocklyEditableText:hover > rect {
|
||||
stroke: var(--primary-color) !important;
|
||||
stroke-width: 2px !important;
|
||||
}
|
||||
|
||||
/* Improve visibility of connection highlights */
|
||||
.blocklyHighlightedConnectionPath {
|
||||
stroke: var(--primary-color) !important;
|
||||
stroke-width: 4px !important;
|
||||
}
|
||||
|
||||
.blocklyInsertionMarker > .blocklyPath {
|
||||
fill-opacity: 0.3 !important;
|
||||
stroke-opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
/* Workspace grid pattern */
|
||||
.blocklyWorkspace > .blocklyBlockCanvas > .blocklyGridCanvas {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
.blocklyDraggable {
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
/* Field labels */
|
||||
.blocklyFieldLabel {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
/* Comment blocks styling */
|
||||
.blocklyCommentText {
|
||||
font-style: italic !important;
|
||||
}
|
||||
|
||||
/* Make comment blocks slightly transparent */
|
||||
g[data-category="Comments"] .blocklyPath {
|
||||
fill-opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
/* Better visibility for disabled blocks */
|
||||
.blocklyDisabled > .blocklyPath {
|
||||
fill-opacity: 0.3 !important;
|
||||
}
|
||||
|
||||
.blocklyDisabled > .blocklyText {
|
||||
fill-opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
/* Warning and error text */
|
||||
.blocklyWarningText,
|
||||
.blocklyErrorText {
|
||||
font-family: 'Dank Mono', monospace !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Workspace scrollbar improvement for dark theme */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
}
|
||||
549
docs/examples/c4a_script/tutorial/assets/c4a-blocks.js
Normal file
549
docs/examples/c4a_script/tutorial/assets/c4a-blocks.js
Normal file
@@ -0,0 +1,549 @@
|
||||
// C4A-Script Blockly Block Definitions
|
||||
// This file defines all custom blocks for C4A-Script commands
|
||||
|
||||
// Color scheme for different block categories
|
||||
const BlockColors = {
|
||||
NAVIGATION: '#1E88E5', // Blue
|
||||
ACTIONS: '#43A047', // Green
|
||||
CONTROL: '#FB8C00', // Orange
|
||||
VARIABLES: '#8E24AA', // Purple
|
||||
WAIT: '#E53935', // Red
|
||||
KEYBOARD: '#00ACC1', // Cyan
|
||||
PROCEDURES: '#6A1B9A' // Deep Purple
|
||||
};
|
||||
|
||||
// Helper to create selector input with backticks
|
||||
Blockly.Blocks['c4a_selector_input'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("selector"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setOutput(true, "Selector");
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("CSS selector for element");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// NAVIGATION BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_go'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("GO")
|
||||
.appendField(new Blockly.FieldTextInput("https://example.com"), "URL");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.NAVIGATION);
|
||||
this.setTooltip("Navigate to URL");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_reload'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("RELOAD");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.NAVIGATION);
|
||||
this.setTooltip("Reload current page");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_back'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("BACK");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.NAVIGATION);
|
||||
this.setTooltip("Go back in browser history");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_forward'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("FORWARD");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.NAVIGATION);
|
||||
this.setTooltip("Go forward in browser history");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// WAIT BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_wait_time'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("WAIT")
|
||||
.appendField(new Blockly.FieldNumber(1, 0), "SECONDS")
|
||||
.appendField("seconds");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.WAIT);
|
||||
this.setTooltip("Wait for specified seconds");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_wait_selector'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("WAIT for")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("selector"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("max")
|
||||
.appendField(new Blockly.FieldNumber(10, 1), "TIMEOUT")
|
||||
.appendField("sec");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.WAIT);
|
||||
this.setTooltip("Wait for element to appear");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_wait_text'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("WAIT for text")
|
||||
.appendField(new Blockly.FieldTextInput("Loading complete"), "TEXT")
|
||||
.appendField("max")
|
||||
.appendField(new Blockly.FieldNumber(5, 1), "TIMEOUT")
|
||||
.appendField("sec");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.WAIT);
|
||||
this.setTooltip("Wait for text to appear on page");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// MOUSE ACTION BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_click'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("CLICK")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("button"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Click on element");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_click_xy'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("CLICK at")
|
||||
.appendField("X:")
|
||||
.appendField(new Blockly.FieldNumber(100, 0), "X")
|
||||
.appendField("Y:")
|
||||
.appendField(new Blockly.FieldNumber(100, 0), "Y");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Click at coordinates");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_double_click'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("DOUBLE_CLICK")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput(".item"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Double click on element");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_right_click'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("RIGHT_CLICK")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("#menu"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Right click on element");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_move'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("MOVE to")
|
||||
.appendField("X:")
|
||||
.appendField(new Blockly.FieldNumber(500, 0), "X")
|
||||
.appendField("Y:")
|
||||
.appendField(new Blockly.FieldNumber(300, 0), "Y");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Move mouse to position");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_drag'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("DRAG from")
|
||||
.appendField("X:")
|
||||
.appendField(new Blockly.FieldNumber(100, 0), "X1")
|
||||
.appendField("Y:")
|
||||
.appendField(new Blockly.FieldNumber(100, 0), "Y1");
|
||||
this.appendDummyInput()
|
||||
.appendField("to")
|
||||
.appendField("X:")
|
||||
.appendField(new Blockly.FieldNumber(500, 0), "X2")
|
||||
.appendField("Y:")
|
||||
.appendField(new Blockly.FieldNumber(300, 0), "Y2");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Drag from one point to another");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_scroll'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("SCROLL")
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
["DOWN", "DOWN"],
|
||||
["UP", "UP"],
|
||||
["LEFT", "LEFT"],
|
||||
["RIGHT", "RIGHT"]
|
||||
]), "DIRECTION")
|
||||
.appendField(new Blockly.FieldNumber(500, 0), "AMOUNT")
|
||||
.appendField("pixels");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.ACTIONS);
|
||||
this.setTooltip("Scroll in direction");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// KEYBOARD BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_type'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("TYPE")
|
||||
.appendField(new Blockly.FieldTextInput("text to type"), "TEXT");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Type text");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_type_var'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("TYPE")
|
||||
.appendField("$")
|
||||
.appendField(new Blockly.FieldTextInput("variable"), "VAR");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Type variable value");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_clear'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("CLEAR")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("input"), "SELECTOR")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Clear input field");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_set'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("SET")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("#input"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("to")
|
||||
.appendField(new Blockly.FieldTextInput("value"), "VALUE");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Set input field value");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_press'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("PRESS")
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
["Tab", "Tab"],
|
||||
["Enter", "Enter"],
|
||||
["Escape", "Escape"],
|
||||
["Space", "Space"],
|
||||
["ArrowUp", "ArrowUp"],
|
||||
["ArrowDown", "ArrowDown"],
|
||||
["ArrowLeft", "ArrowLeft"],
|
||||
["ArrowRight", "ArrowRight"],
|
||||
["Delete", "Delete"],
|
||||
["Backspace", "Backspace"]
|
||||
]), "KEY");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Press and release key");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_key_down'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("KEY_DOWN")
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
["Shift", "Shift"],
|
||||
["Control", "Control"],
|
||||
["Alt", "Alt"],
|
||||
["Meta", "Meta"]
|
||||
]), "KEY");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Hold key down");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_key_up'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("KEY_UP")
|
||||
.appendField(new Blockly.FieldDropdown([
|
||||
["Shift", "Shift"],
|
||||
["Control", "Control"],
|
||||
["Alt", "Alt"],
|
||||
["Meta", "Meta"]
|
||||
]), "KEY");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.KEYBOARD);
|
||||
this.setTooltip("Release key");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// CONTROL FLOW BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_if_exists'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("IF EXISTS")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("THEN");
|
||||
this.appendStatementInput("THEN")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("If element exists, then do something");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_if_exists_else'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("IF EXISTS")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("THEN");
|
||||
this.appendStatementInput("THEN")
|
||||
.setCheck(null);
|
||||
this.appendDummyInput()
|
||||
.appendField("ELSE");
|
||||
this.appendStatementInput("ELSE")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("If element exists, then do something, else do something else");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_if_not_exists'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("IF NOT EXISTS")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput(".element"), "SELECTOR")
|
||||
.appendField("`")
|
||||
.appendField("THEN");
|
||||
this.appendStatementInput("THEN")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("If element does not exist, then do something");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_if_js'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("IF")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("window.innerWidth < 768"), "CONDITION")
|
||||
.appendField("`")
|
||||
.appendField("THEN");
|
||||
this.appendStatementInput("THEN")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("If JavaScript condition is true");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_repeat_times'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("REPEAT")
|
||||
.appendField(new Blockly.FieldNumber(5, 1), "TIMES")
|
||||
.appendField("times");
|
||||
this.appendStatementInput("DO")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("Repeat commands N times");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_repeat_while'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("REPEAT WHILE")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("document.querySelector('.load-more')"), "CONDITION")
|
||||
.appendField("`");
|
||||
this.appendStatementInput("DO")
|
||||
.setCheck(null);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.CONTROL);
|
||||
this.setTooltip("Repeat while condition is true");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// VARIABLE BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_setvar'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("SETVAR")
|
||||
.appendField(new Blockly.FieldTextInput("username"), "NAME")
|
||||
.appendField("=")
|
||||
.appendField(new Blockly.FieldTextInput("value"), "VALUE");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.VARIABLES);
|
||||
this.setTooltip("Set variable value");
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ADVANCED BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_eval'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("EVAL")
|
||||
.appendField("`")
|
||||
.appendField(new Blockly.FieldTextInput("console.log('Hello')"), "CODE")
|
||||
.appendField("`");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.VARIABLES);
|
||||
this.setTooltip("Execute JavaScript code");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_comment'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("#")
|
||||
.appendField(new Blockly.FieldTextInput("Comment", null, {
|
||||
spellcheck: false,
|
||||
class: 'blocklyCommentText'
|
||||
}), "TEXT");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour("#616161");
|
||||
this.setTooltip("Add a comment");
|
||||
this.setStyle('comment_blocks');
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PROCEDURE BLOCKS
|
||||
// ============================================
|
||||
|
||||
Blockly.Blocks['c4a_proc_def'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("PROC")
|
||||
.appendField(new Blockly.FieldTextInput("procedure_name"), "NAME");
|
||||
this.appendStatementInput("BODY")
|
||||
.setCheck(null);
|
||||
this.appendDummyInput()
|
||||
.appendField("ENDPROC");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.PROCEDURES);
|
||||
this.setTooltip("Define a procedure");
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['c4a_proc_call'] = {
|
||||
init: function() {
|
||||
this.appendDummyInput()
|
||||
.appendField("Call")
|
||||
.appendField(new Blockly.FieldTextInput("procedure_name"), "NAME");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(BlockColors.PROCEDURES);
|
||||
this.setTooltip("Call a procedure");
|
||||
}
|
||||
};
|
||||
|
||||
// Code generators have been moved to c4a-generator.js
|
||||
261
docs/examples/c4a_script/tutorial/assets/c4a-generator.js
Normal file
261
docs/examples/c4a_script/tutorial/assets/c4a-generator.js
Normal file
@@ -0,0 +1,261 @@
|
||||
// C4A-Script Code Generator for Blockly
|
||||
// Compatible with latest Blockly API
|
||||
|
||||
// Create a custom code generator for C4A-Script
|
||||
const c4aGenerator = new Blockly.Generator('C4A');
|
||||
|
||||
// Helper to get field value with proper escaping
|
||||
c4aGenerator.getFieldValue = function(block, fieldName) {
|
||||
return block.getFieldValue(fieldName);
|
||||
};
|
||||
|
||||
// Navigation generators
|
||||
c4aGenerator.forBlock['c4a_go'] = function(block, generator) {
|
||||
const url = generator.getFieldValue(block, 'URL');
|
||||
return `GO ${url}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_reload'] = function(block, generator) {
|
||||
return 'RELOAD\n';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_back'] = function(block, generator) {
|
||||
return 'BACK\n';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_forward'] = function(block, generator) {
|
||||
return 'FORWARD\n';
|
||||
};
|
||||
|
||||
// Wait generators
|
||||
c4aGenerator.forBlock['c4a_wait_time'] = function(block, generator) {
|
||||
const seconds = generator.getFieldValue(block, 'SECONDS');
|
||||
return `WAIT ${seconds}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_wait_selector'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const timeout = generator.getFieldValue(block, 'TIMEOUT');
|
||||
return `WAIT \`${selector}\` ${timeout}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_wait_text'] = function(block, generator) {
|
||||
const text = generator.getFieldValue(block, 'TEXT');
|
||||
const timeout = generator.getFieldValue(block, 'TIMEOUT');
|
||||
return `WAIT "${text}" ${timeout}\n`;
|
||||
};
|
||||
|
||||
// Mouse action generators
|
||||
c4aGenerator.forBlock['c4a_click'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
return `CLICK \`${selector}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_click_xy'] = function(block, generator) {
|
||||
const x = generator.getFieldValue(block, 'X');
|
||||
const y = generator.getFieldValue(block, 'Y');
|
||||
return `CLICK ${x} ${y}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_double_click'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
return `DOUBLE_CLICK \`${selector}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_right_click'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
return `RIGHT_CLICK \`${selector}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_move'] = function(block, generator) {
|
||||
const x = generator.getFieldValue(block, 'X');
|
||||
const y = generator.getFieldValue(block, 'Y');
|
||||
return `MOVE ${x} ${y}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_drag'] = function(block, generator) {
|
||||
const x1 = generator.getFieldValue(block, 'X1');
|
||||
const y1 = generator.getFieldValue(block, 'Y1');
|
||||
const x2 = generator.getFieldValue(block, 'X2');
|
||||
const y2 = generator.getFieldValue(block, 'Y2');
|
||||
return `DRAG ${x1} ${y1} ${x2} ${y2}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_scroll'] = function(block, generator) {
|
||||
const direction = generator.getFieldValue(block, 'DIRECTION');
|
||||
const amount = generator.getFieldValue(block, 'AMOUNT');
|
||||
return `SCROLL ${direction} ${amount}\n`;
|
||||
};
|
||||
|
||||
// Keyboard generators
|
||||
c4aGenerator.forBlock['c4a_type'] = function(block, generator) {
|
||||
const text = generator.getFieldValue(block, 'TEXT');
|
||||
return `TYPE "${text}"\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_type_var'] = function(block, generator) {
|
||||
const varName = generator.getFieldValue(block, 'VAR');
|
||||
return `TYPE $${varName}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_clear'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
return `CLEAR \`${selector}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_set'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const value = generator.getFieldValue(block, 'VALUE');
|
||||
return `SET \`${selector}\` "${value}"\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_press'] = function(block, generator) {
|
||||
const key = generator.getFieldValue(block, 'KEY');
|
||||
return `PRESS ${key}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_key_down'] = function(block, generator) {
|
||||
const key = generator.getFieldValue(block, 'KEY');
|
||||
return `KEY_DOWN ${key}\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_key_up'] = function(block, generator) {
|
||||
const key = generator.getFieldValue(block, 'KEY');
|
||||
return `KEY_UP ${key}\n`;
|
||||
};
|
||||
|
||||
// Control flow generators
|
||||
c4aGenerator.forBlock['c4a_if_exists'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const thenCode = generator.statementToCode(block, 'THEN').trim();
|
||||
|
||||
if (thenCode.includes('\n')) {
|
||||
// Multi-line then block
|
||||
const lines = thenCode.split('\n').filter(line => line.trim());
|
||||
return lines.map(line => `IF (EXISTS \`${selector}\`) THEN ${line}`).join('\n') + '\n';
|
||||
} else if (thenCode) {
|
||||
// Single line
|
||||
return `IF (EXISTS \`${selector}\`) THEN ${thenCode}\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_if_exists_else'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const thenCode = generator.statementToCode(block, 'THEN').trim();
|
||||
const elseCode = generator.statementToCode(block, 'ELSE').trim();
|
||||
|
||||
// For simplicity, only handle single-line then/else
|
||||
const thenLine = thenCode.split('\n')[0];
|
||||
const elseLine = elseCode.split('\n')[0];
|
||||
|
||||
if (thenLine && elseLine) {
|
||||
return `IF (EXISTS \`${selector}\`) THEN ${thenLine} ELSE ${elseLine}\n`;
|
||||
} else if (thenLine) {
|
||||
return `IF (EXISTS \`${selector}\`) THEN ${thenLine}\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_if_not_exists'] = function(block, generator) {
|
||||
const selector = generator.getFieldValue(block, 'SELECTOR');
|
||||
const thenCode = generator.statementToCode(block, 'THEN').trim();
|
||||
|
||||
if (thenCode.includes('\n')) {
|
||||
const lines = thenCode.split('\n').filter(line => line.trim());
|
||||
return lines.map(line => `IF (NOT EXISTS \`${selector}\`) THEN ${line}`).join('\n') + '\n';
|
||||
} else if (thenCode) {
|
||||
return `IF (NOT EXISTS \`${selector}\`) THEN ${thenCode}\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_if_js'] = function(block, generator) {
|
||||
const condition = generator.getFieldValue(block, 'CONDITION');
|
||||
const thenCode = generator.statementToCode(block, 'THEN').trim();
|
||||
|
||||
if (thenCode.includes('\n')) {
|
||||
const lines = thenCode.split('\n').filter(line => line.trim());
|
||||
return lines.map(line => `IF (\`${condition}\`) THEN ${line}`).join('\n') + '\n';
|
||||
} else if (thenCode) {
|
||||
return `IF (\`${condition}\`) THEN ${thenCode}\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_repeat_times'] = function(block, generator) {
|
||||
const times = generator.getFieldValue(block, 'TIMES');
|
||||
const doCode = generator.statementToCode(block, 'DO').trim();
|
||||
|
||||
if (doCode) {
|
||||
// Get first command for repeat
|
||||
const firstLine = doCode.split('\n')[0];
|
||||
return `REPEAT (${firstLine}, ${times})\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_repeat_while'] = function(block, generator) {
|
||||
const condition = generator.getFieldValue(block, 'CONDITION');
|
||||
const doCode = generator.statementToCode(block, 'DO').trim();
|
||||
|
||||
if (doCode) {
|
||||
// Get first command for repeat
|
||||
const firstLine = doCode.split('\n')[0];
|
||||
return `REPEAT (${firstLine}, \`${condition}\`)\n`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Variable generators
|
||||
c4aGenerator.forBlock['c4a_setvar'] = function(block, generator) {
|
||||
const name = generator.getFieldValue(block, 'NAME');
|
||||
const value = generator.getFieldValue(block, 'VALUE');
|
||||
return `SETVAR ${name} = "${value}"\n`;
|
||||
};
|
||||
|
||||
// Advanced generators
|
||||
c4aGenerator.forBlock['c4a_eval'] = function(block, generator) {
|
||||
const code = generator.getFieldValue(block, 'CODE');
|
||||
return `EVAL \`${code}\`\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_comment'] = function(block, generator) {
|
||||
const text = generator.getFieldValue(block, 'TEXT');
|
||||
return `# ${text}\n`;
|
||||
};
|
||||
|
||||
// Procedure generators
|
||||
c4aGenerator.forBlock['c4a_proc_def'] = function(block, generator) {
|
||||
const name = generator.getFieldValue(block, 'NAME');
|
||||
const body = generator.statementToCode(block, 'BODY');
|
||||
return `PROC ${name}\n${body}ENDPROC\n`;
|
||||
};
|
||||
|
||||
c4aGenerator.forBlock['c4a_proc_call'] = function(block, generator) {
|
||||
const name = generator.getFieldValue(block, 'NAME');
|
||||
return `${name}\n`;
|
||||
};
|
||||
|
||||
// Override scrub_ to handle our custom format
|
||||
c4aGenerator.scrub_ = function(block, code, opt_thisOnly) {
|
||||
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
|
||||
let nextCode = '';
|
||||
|
||||
if (nextBlock) {
|
||||
if (!opt_thisOnly) {
|
||||
nextCode = c4aGenerator.blockToCode(nextBlock);
|
||||
|
||||
// Add blank line between comment and non-comment blocks
|
||||
const currentIsComment = block.type === 'c4a_comment';
|
||||
const nextIsComment = nextBlock.type === 'c4a_comment';
|
||||
|
||||
// Add blank line when transitioning from command to comment or vice versa
|
||||
if (currentIsComment !== nextIsComment && code.trim() && nextCode.trim()) {
|
||||
nextCode = '\n' + nextCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return code + nextCode;
|
||||
};
|
||||
21
docs/examples/c4a_script/tutorial/blockly-demo.c4a
Normal file
21
docs/examples/c4a_script/tutorial/blockly-demo.c4a
Normal file
@@ -0,0 +1,21 @@
|
||||
# Demo: Login Flow with Blockly
|
||||
# This script can be created visually using Blockly blocks
|
||||
|
||||
GO https://example.com/login
|
||||
WAIT `#login-form` 5
|
||||
|
||||
# Check if already logged in
|
||||
IF (EXISTS `.user-avatar`) THEN GO https://example.com/dashboard
|
||||
|
||||
# Fill login form
|
||||
CLICK `#email`
|
||||
TYPE "demo@example.com"
|
||||
CLICK `#password`
|
||||
TYPE "password123"
|
||||
|
||||
# Submit form
|
||||
CLICK `button[type="submit"]`
|
||||
WAIT `.dashboard` 10
|
||||
|
||||
# Success message
|
||||
EVAL `console.log('Login successful!')`
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>C4A-Script Interactive Tutorial | Crawl4AI</title>
|
||||
<link rel="stylesheet" href="assets/app.css">
|
||||
<link rel="stylesheet" href="assets/blockly-theme.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/material-darker.min.css">
|
||||
</head>
|
||||
@@ -25,6 +26,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Editor Modal -->
|
||||
<div id="event-editor-overlay" class="modal-overlay hidden"></div>
|
||||
<div id="event-editor-modal" class="event-editor-modal hidden">
|
||||
<h4>Edit Event</h4>
|
||||
<div class="editor-field">
|
||||
<label>Command Type</label>
|
||||
<select id="edit-command-type" disabled>
|
||||
<option value="CLICK">CLICK</option>
|
||||
<option value="DOUBLE_CLICK">DOUBLE_CLICK</option>
|
||||
<option value="RIGHT_CLICK">RIGHT_CLICK</option>
|
||||
<option value="TYPE">TYPE</option>
|
||||
<option value="SET">SET</option>
|
||||
<option value="SCROLL">SCROLL</option>
|
||||
<option value="WAIT">WAIT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="edit-selector-field" class="editor-field">
|
||||
<label>Selector</label>
|
||||
<input type="text" id="edit-selector" placeholder=".class or #id">
|
||||
</div>
|
||||
<div id="edit-value-field" class="editor-field">
|
||||
<label>Value</label>
|
||||
<input type="text" id="edit-value" placeholder="Text or number">
|
||||
</div>
|
||||
<div id="edit-direction-field" class="editor-field hidden">
|
||||
<label>Direction</label>
|
||||
<select id="edit-direction">
|
||||
<option value="UP">UP</option>
|
||||
<option value="DOWN">DOWN</option>
|
||||
<option value="LEFT">LEFT</option>
|
||||
<option value="RIGHT">RIGHT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<button id="edit-cancel" class="mini-btn">Cancel</button>
|
||||
<button id="edit-save" class="mini-btn primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main App Layout -->
|
||||
<div class="app-container">
|
||||
@@ -45,11 +85,35 @@
|
||||
<button id="run-btn" class="action-btn primary">
|
||||
<span class="icon">▶</span>Run
|
||||
</button>
|
||||
<button id="record-btn" class="action-btn record">
|
||||
<span class="icon">⏺</span>Record
|
||||
</button>
|
||||
<button id="timeline-btn" class="action-btn timeline hidden" title="View Timeline">
|
||||
<span class="icon">📊</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-wrapper">
|
||||
<textarea id="c4a-editor" placeholder="# Write your C4A script here..."></textarea>
|
||||
<div class="editor-container">
|
||||
<div id="editor-view" class="editor-wrapper">
|
||||
<textarea id="c4a-editor" placeholder="# Write your C4A script here..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Recording Timeline -->
|
||||
<div id="timeline-view" class="recording-timeline hidden">
|
||||
<div class="timeline-header">
|
||||
<h3>Recording Timeline</h3>
|
||||
<div class="timeline-actions">
|
||||
<button id="back-to-editor" class="mini-btn">← Back</button>
|
||||
<button id="select-all-events" class="mini-btn">Select All</button>
|
||||
<button id="clear-events" class="mini-btn">Clear</button>
|
||||
<button id="generate-script" class="mini-btn primary">Generate Script</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="timeline-events" class="timeline-events">
|
||||
<!-- Events will be added here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: Output Tabs -->
|
||||
@@ -129,6 +193,13 @@
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/mode/javascript/javascript.min.js"></script>
|
||||
|
||||
<!-- Blockly -->
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="assets/c4a-blocks.js"></script>
|
||||
<script src="assets/c4a-generator.js"></script>
|
||||
<script src="assets/blockly-manager.js"></script>
|
||||
|
||||
<script src="assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
docs/examples/c4a_script/tutorial/test_blockly.html
Normal file
69
docs/examples/c4a_script/tutorial/test_blockly.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Blockly Test</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #0e0e10;
|
||||
color: #e0e0e0;
|
||||
font-family: monospace;
|
||||
}
|
||||
#blocklyDiv {
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
border: 1px solid #2a2a2c;
|
||||
}
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #1a1a1b;
|
||||
border: 1px solid #2a2a2c;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>C4A-Script Blockly Test</h1>
|
||||
<div id="blocklyDiv"></div>
|
||||
<div id="output">
|
||||
<h3>Generated C4A-Script:</h3>
|
||||
<pre id="code-output"></pre>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/blockly/blockly.min.js"></script>
|
||||
<script src="assets/c4a-blocks.js"></script>
|
||||
<script>
|
||||
// Simple test
|
||||
const workspace = Blockly.inject('blocklyDiv', {
|
||||
toolbox: `
|
||||
<xml>
|
||||
<category name="Test" colour="#1E88E5">
|
||||
<block type="c4a_go"></block>
|
||||
<block type="c4a_wait_time"></block>
|
||||
<block type="c4a_click"></block>
|
||||
</category>
|
||||
</xml>
|
||||
`,
|
||||
theme: Blockly.Theme.defineTheme('dark', {
|
||||
'base': Blockly.Themes.Classic,
|
||||
'componentStyles': {
|
||||
'workspaceBackgroundColour': '#0e0e10',
|
||||
'toolboxBackgroundColour': '#1a1a1b',
|
||||
'toolboxForegroundColour': '#e0e0e0',
|
||||
'flyoutBackgroundColour': '#1a1a1b',
|
||||
'flyoutForegroundColour': '#e0e0e0',
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
workspace.addChangeListener((event) => {
|
||||
const code = Blockly.JavaScript.workspaceToCode(workspace);
|
||||
document.getElementById('code-output').textContent = code;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
992
docs/md_v2/api/c4a-script-reference.md
Normal file
992
docs/md_v2/api/c4a-script-reference.md
Normal file
@@ -0,0 +1,992 @@
|
||||
# C4A-Script API Reference
|
||||
|
||||
Complete reference for all C4A-Script commands, syntax, and advanced features.
|
||||
|
||||
## Command Categories
|
||||
|
||||
### 🧭 Navigation Commands
|
||||
|
||||
Navigate between pages and manage browser history.
|
||||
|
||||
#### `GO <url>`
|
||||
Navigate to a specific URL.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
GO <url>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `url` - Target URL (string)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
GO https://example.com
|
||||
GO https://api.example.com/login
|
||||
GO /relative/path
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Supports both absolute and relative URLs
|
||||
- Automatically handles protocol detection
|
||||
- Waits for page load to complete
|
||||
|
||||
---
|
||||
|
||||
#### `RELOAD`
|
||||
Refresh the current page.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
RELOAD
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
RELOAD
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Equivalent to pressing F5 or clicking browser refresh
|
||||
- Waits for page reload to complete
|
||||
- Preserves current URL
|
||||
|
||||
---
|
||||
|
||||
#### `BACK`
|
||||
Navigate back in browser history.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
BACK
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
BACK
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Equivalent to clicking browser back button
|
||||
- Does nothing if no previous page exists
|
||||
- Waits for navigation to complete
|
||||
|
||||
---
|
||||
|
||||
#### `FORWARD`
|
||||
Navigate forward in browser history.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
FORWARD
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
FORWARD
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Equivalent to clicking browser forward button
|
||||
- Does nothing if no next page exists
|
||||
- Waits for navigation to complete
|
||||
|
||||
### ⏱️ Wait Commands
|
||||
|
||||
Control timing and synchronization with page elements.
|
||||
|
||||
#### `WAIT <time>`
|
||||
Wait for a specified number of seconds.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
WAIT <seconds>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `seconds` - Number of seconds to wait (number)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
WAIT 3
|
||||
WAIT 1.5
|
||||
WAIT 10
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Accepts decimal values
|
||||
- Useful for giving dynamic content time to load
|
||||
- Non-blocking for other browser operations
|
||||
|
||||
---
|
||||
|
||||
#### `WAIT <selector> <timeout>`
|
||||
Wait for an element to appear on the page.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
WAIT `<selector>` <timeout>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector for the element (string in backticks)
|
||||
- `timeout` - Maximum seconds to wait (number)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
WAIT `#content` 10
|
||||
WAIT `.loading-spinner` 5
|
||||
WAIT `button[type="submit"]` 15
|
||||
WAIT `.results .item:first-child` 8
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Fails if element doesn't appear within timeout
|
||||
- More reliable than fixed time waits
|
||||
- Supports complex CSS selectors
|
||||
|
||||
---
|
||||
|
||||
#### `WAIT "<text>" <timeout>`
|
||||
Wait for specific text to appear anywhere on the page.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
WAIT "<text>" <timeout>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `text` - Text content to wait for (string in quotes)
|
||||
- `timeout` - Maximum seconds to wait (number)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
WAIT "Loading complete" 10
|
||||
WAIT "Welcome back" 5
|
||||
WAIT "Search results" 15
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Case-sensitive text matching
|
||||
- Searches entire page content
|
||||
- Useful for dynamic status messages
|
||||
|
||||
### 🖱️ Mouse Commands
|
||||
|
||||
Simulate mouse interactions and movements.
|
||||
|
||||
#### `CLICK <selector>`
|
||||
Click on an element specified by CSS selector.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
CLICK `<selector>`
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector for the element (string in backticks)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
CLICK `#submit-button`
|
||||
CLICK `.menu-item:first-child`
|
||||
CLICK `button[data-action="save"]`
|
||||
CLICK `a[href="/dashboard"]`
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Waits for element to be clickable
|
||||
- Scrolls element into view if necessary
|
||||
- Handles overlapping elements intelligently
|
||||
|
||||
---
|
||||
|
||||
#### `CLICK <x> <y>`
|
||||
Click at specific coordinates on the page.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
CLICK <x> <y>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `x` - X coordinate in pixels (number)
|
||||
- `y` - Y coordinate in pixels (number)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
CLICK 100 200
|
||||
CLICK 500 300
|
||||
CLICK 0 0
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Coordinates are relative to viewport
|
||||
- Useful when element selectors are unreliable
|
||||
- Consider responsive design implications
|
||||
|
||||
---
|
||||
|
||||
#### `DOUBLE_CLICK <selector>`
|
||||
Double-click on an element.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
DOUBLE_CLICK `<selector>`
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector for the element (string in backticks)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
DOUBLE_CLICK `.file-icon`
|
||||
DOUBLE_CLICK `#editable-cell`
|
||||
DOUBLE_CLICK `.expandable-item`
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Triggers dblclick event
|
||||
- Common for opening files or editing inline content
|
||||
- Timing between clicks is automatically handled
|
||||
|
||||
---
|
||||
|
||||
#### `RIGHT_CLICK <selector>`
|
||||
Right-click on an element to open context menu.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
RIGHT_CLICK `<selector>`
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector for the element (string in backticks)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
RIGHT_CLICK `#context-target`
|
||||
RIGHT_CLICK `.menu-trigger`
|
||||
RIGHT_CLICK `img.thumbnail`
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Opens browser/application context menu
|
||||
- Useful for testing context menu interactions
|
||||
- May be blocked by some applications
|
||||
|
||||
---
|
||||
|
||||
#### `SCROLL <direction> <amount>`
|
||||
Scroll the page in a specified direction.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
SCROLL <direction> <amount>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `direction` - Direction to scroll: `UP`, `DOWN`, `LEFT`, `RIGHT`
|
||||
- `amount` - Number of pixels to scroll (number)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
SCROLL DOWN 500
|
||||
SCROLL UP 200
|
||||
SCROLL LEFT 100
|
||||
SCROLL RIGHT 300
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Smooth scrolling animation
|
||||
- Useful for infinite scroll pages
|
||||
- Amount can be larger than viewport
|
||||
|
||||
---
|
||||
|
||||
#### `MOVE <x> <y>`
|
||||
Move mouse cursor to specific coordinates.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
MOVE <x> <y>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `x` - X coordinate in pixels (number)
|
||||
- `y` - Y coordinate in pixels (number)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
MOVE 200 100
|
||||
MOVE 500 400
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Triggers hover effects
|
||||
- Useful for testing mouseover interactions
|
||||
- Does not click, only moves cursor
|
||||
|
||||
---
|
||||
|
||||
#### `DRAG <x1> <y1> <x2> <y2>`
|
||||
Drag from one point to another.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
DRAG <x1> <y1> <x2> <y2>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `x1`, `y1` - Starting coordinates (numbers)
|
||||
- `x2`, `y2` - Ending coordinates (numbers)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
DRAG 100 100 500 300
|
||||
DRAG 0 200 400 200
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Simulates click, drag, and release
|
||||
- Useful for sliders, resizing, reordering
|
||||
- Smooth drag animation
|
||||
|
||||
### ⌨️ Keyboard Commands
|
||||
|
||||
Simulate keyboard input and key presses.
|
||||
|
||||
#### `TYPE "<text>"`
|
||||
Type text into the currently focused element.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
TYPE "<text>"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `text` - Text to type (string in quotes)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
TYPE "Hello, World!"
|
||||
TYPE "user@example.com"
|
||||
TYPE "Password123!"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Requires an input element to be focused
|
||||
- Types character by character with realistic timing
|
||||
- Supports special characters and Unicode
|
||||
|
||||
---
|
||||
|
||||
#### `TYPE $<variable>`
|
||||
Type the value of a variable.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
TYPE $<variable>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `variable` - Variable name (without quotes)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
SETVAR email = "user@example.com"
|
||||
TYPE $email
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Variable must be defined with SETVAR first
|
||||
- Variable values are strings
|
||||
- Useful for reusable credentials or data
|
||||
|
||||
---
|
||||
|
||||
#### `PRESS <key>`
|
||||
Press and release a special key.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
PRESS <key>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key` - Key name (see supported keys below)
|
||||
|
||||
**Supported Keys:**
|
||||
- `Tab`, `Enter`, `Escape`, `Space`
|
||||
- `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight`
|
||||
- `Delete`, `Backspace`
|
||||
- `Home`, `End`, `PageUp`, `PageDown`
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
PRESS Tab
|
||||
PRESS Enter
|
||||
PRESS Escape
|
||||
PRESS ArrowDown
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Simulates actual key press and release
|
||||
- Useful for form navigation and shortcuts
|
||||
- Case-sensitive key names
|
||||
|
||||
---
|
||||
|
||||
#### `KEY_DOWN <key>`
|
||||
Hold down a modifier key.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
KEY_DOWN <key>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key` - Modifier key: `Shift`, `Control`, `Alt`, `Meta`
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
KEY_DOWN Shift
|
||||
KEY_DOWN Control
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Must be paired with KEY_UP
|
||||
- Useful for key combinations
|
||||
- Meta key is Cmd on Mac, Windows key on PC
|
||||
|
||||
---
|
||||
|
||||
#### `KEY_UP <key>`
|
||||
Release a modifier key.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
KEY_UP <key>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `key` - Modifier key: `Shift`, `Control`, `Alt`, `Meta`
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
KEY_UP Shift
|
||||
KEY_UP Control
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Must be paired with KEY_DOWN
|
||||
- Releases the specified modifier key
|
||||
- Good practice to always release held keys
|
||||
|
||||
---
|
||||
|
||||
#### `CLEAR <selector>`
|
||||
Clear the content of an input field.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
CLEAR `<selector>`
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector for input element (string in backticks)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
CLEAR `#search-box`
|
||||
CLEAR `input[name="email"]`
|
||||
CLEAR `.form-input:first-child`
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Works with input, textarea elements
|
||||
- Faster than selecting all and deleting
|
||||
- Triggers appropriate change events
|
||||
|
||||
---
|
||||
|
||||
#### `SET <selector> "<value>"`
|
||||
Set the value of an input field directly.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
SET `<selector>` "<value>"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector for input element (string in backticks)
|
||||
- `value` - Value to set (string in quotes)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
SET `#email` "user@example.com"
|
||||
SET `#age` "25"
|
||||
SET `textarea#message` "Hello, this is a test message."
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Directly sets value without typing animation
|
||||
- Faster than TYPE for long text
|
||||
- Triggers change and input events
|
||||
|
||||
### 🔀 Control Flow Commands
|
||||
|
||||
Add conditional logic and loops to your scripts.
|
||||
|
||||
#### `IF (EXISTS <selector>) THEN <command>`
|
||||
Execute command if element exists.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
IF (EXISTS `<selector>`) THEN <command>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector to check (string in backticks)
|
||||
- `command` - Command to execute if condition is true
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-cookies`
|
||||
IF (EXISTS `#popup-modal`) THEN CLICK `.close-button`
|
||||
IF (EXISTS `.error-message`) THEN RELOAD
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Checks for element existence at time of execution
|
||||
- Does not wait for element to appear
|
||||
- Can be combined with ELSE
|
||||
|
||||
---
|
||||
|
||||
#### `IF (EXISTS <selector>) THEN <command> ELSE <command>`
|
||||
Execute command based on element existence.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
IF (EXISTS `<selector>`) THEN <command> ELSE <command>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector to check (string in backticks)
|
||||
- First `command` - Execute if condition is true
|
||||
- Second `command` - Execute if condition is false
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
IF (EXISTS `.user-menu`) THEN CLICK `.logout` ELSE CLICK `.login`
|
||||
IF (EXISTS `.loading`) THEN WAIT 5 ELSE CLICK `#continue`
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Exactly one command will be executed
|
||||
- Useful for handling different page states
|
||||
- Commands must be on same line
|
||||
|
||||
---
|
||||
|
||||
#### `IF (NOT EXISTS <selector>) THEN <command>`
|
||||
Execute command if element does not exist.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
IF (NOT EXISTS `<selector>`) THEN <command>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `selector` - CSS selector to check (string in backticks)
|
||||
- `command` - Command to execute if element doesn't exist
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
IF (NOT EXISTS `.logged-in`) THEN GO /login
|
||||
IF (NOT EXISTS `.results`) THEN CLICK `#search-button`
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Inverse of EXISTS condition
|
||||
- Useful for error handling
|
||||
- Can check for missing required elements
|
||||
|
||||
---
|
||||
|
||||
#### `IF (<javascript>) THEN <command>`
|
||||
Execute command based on JavaScript condition.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
IF (`<javascript>`) THEN <command>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `javascript` - JavaScript expression that returns boolean (string in backticks)
|
||||
- `command` - Command to execute if condition is true
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
IF (`window.innerWidth < 768`) THEN CLICK `.mobile-menu`
|
||||
IF (`document.readyState === "complete"`) THEN CLICK `#start`
|
||||
IF (`localStorage.getItem("user")`) THEN GO /dashboard
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- JavaScript executes in browser context
|
||||
- Must return boolean value
|
||||
- Access to all browser APIs and globals
|
||||
|
||||
---
|
||||
|
||||
#### `REPEAT (<command>, <count>)`
|
||||
Repeat a command a specific number of times.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
REPEAT (<command>, <count>)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `command` - Command to repeat
|
||||
- `count` - Number of times to repeat (number)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
REPEAT (SCROLL DOWN 300, 5)
|
||||
REPEAT (PRESS Tab, 3)
|
||||
REPEAT (CLICK `.load-more`, 10)
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Executes command exactly count times
|
||||
- Useful for pagination, scrolling, navigation
|
||||
- No delay between repetitions (add WAIT if needed)
|
||||
|
||||
---
|
||||
|
||||
#### `REPEAT (<command>, <condition>)`
|
||||
Repeat a command while condition is true.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
REPEAT (<command>, `<condition>`)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `command` - Command to repeat
|
||||
- `condition` - JavaScript condition to check (string in backticks)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
REPEAT (SCROLL DOWN 500, `document.querySelector(".load-more")`)
|
||||
REPEAT (PRESS ArrowDown, `window.scrollY < document.body.scrollHeight`)
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Condition checked before each iteration
|
||||
- JavaScript condition must return boolean
|
||||
- Be careful to avoid infinite loops
|
||||
|
||||
### 💾 Variables and Data
|
||||
|
||||
Store and manipulate data within scripts.
|
||||
|
||||
#### `SETVAR <name> = "<value>"`
|
||||
Create or update a variable.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
SETVAR <name> = "<value>"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` - Variable name (alphanumeric, underscore)
|
||||
- `value` - Variable value (string in quotes)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
SETVAR username = "john@example.com"
|
||||
SETVAR password = "secret123"
|
||||
SETVAR base_url = "https://api.example.com"
|
||||
SETVAR counter = "0"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Variables are global within script scope
|
||||
- Values are always strings
|
||||
- Can be used with TYPE command using $variable syntax
|
||||
|
||||
---
|
||||
|
||||
#### `EVAL <javascript>`
|
||||
Execute arbitrary JavaScript code.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
EVAL `<javascript>`
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `javascript` - JavaScript code to execute (string in backticks)
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
EVAL `console.log("Script started")`
|
||||
EVAL `window.scrollTo(0, 0)`
|
||||
EVAL `localStorage.setItem("test", "value")`
|
||||
EVAL `document.title = "Automated Test"`
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Full access to browser JavaScript APIs
|
||||
- Useful for custom logic and debugging
|
||||
- Return values are not captured
|
||||
- Be careful with security implications
|
||||
|
||||
### 📝 Comments and Documentation
|
||||
|
||||
#### `# <comment>`
|
||||
Add comments to scripts for documentation.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
# <comment text>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
# This script logs into the application
|
||||
# Step 1: Navigate to login page
|
||||
GO /login
|
||||
|
||||
# Step 2: Fill credentials
|
||||
TYPE "user@example.com"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Comments are ignored during execution
|
||||
- Useful for documentation and debugging
|
||||
- Can appear anywhere in script
|
||||
- Supports multi-line documentation blocks
|
||||
|
||||
### 🔧 Procedures (Advanced)
|
||||
|
||||
Define reusable command sequences.
|
||||
|
||||
#### `PROC <name> ... ENDPROC`
|
||||
Define a reusable procedure.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
PROC <name>
|
||||
<commands>
|
||||
ENDPROC
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` - Procedure name (alphanumeric, underscore)
|
||||
- `commands` - Commands to include in procedure
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
PROC login
|
||||
CLICK `#email`
|
||||
TYPE $email
|
||||
CLICK `#password`
|
||||
TYPE $password
|
||||
CLICK `#submit`
|
||||
ENDPROC
|
||||
|
||||
PROC handle_popups
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
|
||||
IF (EXISTS `.newsletter-modal`) THEN CLICK `.close`
|
||||
ENDPROC
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Procedures must be defined before use
|
||||
- Support nested command structures
|
||||
- Variables are shared with main script scope
|
||||
|
||||
---
|
||||
|
||||
#### `<procedure_name>`
|
||||
Call a defined procedure.
|
||||
|
||||
**Syntax:**
|
||||
```c4a
|
||||
<procedure_name>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```c4a
|
||||
# Define procedure first
|
||||
PROC setup
|
||||
GO /login
|
||||
WAIT `#form` 5
|
||||
ENDPROC
|
||||
|
||||
# Call procedure
|
||||
setup
|
||||
login
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Procedure must be defined before calling
|
||||
- Can be called multiple times
|
||||
- No parameters supported (use variables instead)
|
||||
|
||||
## Error Handling Best Practices
|
||||
|
||||
### 1. Always Use Waits
|
||||
```c4a
|
||||
# Bad - element might not be ready
|
||||
CLICK `#button`
|
||||
|
||||
# Good - wait for element first
|
||||
WAIT `#button` 5
|
||||
CLICK `#button`
|
||||
```
|
||||
|
||||
### 2. Handle Optional Elements
|
||||
```c4a
|
||||
# Check before interacting
|
||||
IF (EXISTS `.popup`) THEN CLICK `.close`
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept`
|
||||
|
||||
# Then proceed with main flow
|
||||
CLICK `#main-action`
|
||||
```
|
||||
|
||||
### 3. Use Descriptive Variables
|
||||
```c4a
|
||||
# Set up reusable data
|
||||
SETVAR admin_email = "admin@company.com"
|
||||
SETVAR test_password = "TestPass123!"
|
||||
SETVAR staging_url = "https://staging.example.com"
|
||||
|
||||
# Use throughout script
|
||||
GO $staging_url
|
||||
TYPE $admin_email
|
||||
```
|
||||
|
||||
### 4. Add Debugging Information
|
||||
```c4a
|
||||
# Log progress
|
||||
EVAL `console.log("Starting login process")`
|
||||
GO /login
|
||||
|
||||
# Verify page state
|
||||
IF (`document.title.includes("Login")`) THEN EVAL `console.log("On login page")`
|
||||
|
||||
# Continue with login
|
||||
TYPE $username
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Login Flow
|
||||
```c4a
|
||||
# Complete login automation
|
||||
SETVAR email = "user@example.com"
|
||||
SETVAR password = "mypassword"
|
||||
|
||||
GO /login
|
||||
WAIT `#login-form` 5
|
||||
|
||||
# Handle optional cookie banner
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-cookies`
|
||||
|
||||
# Fill and submit form
|
||||
CLICK `#email`
|
||||
TYPE $email
|
||||
PRESS Tab
|
||||
TYPE $password
|
||||
CLICK `button[type="submit"]`
|
||||
|
||||
# Wait for redirect
|
||||
WAIT `.dashboard` 10
|
||||
```
|
||||
|
||||
### Infinite Scroll
|
||||
```c4a
|
||||
# Load all content with infinite scroll
|
||||
GO /products
|
||||
|
||||
# Scroll and load more content
|
||||
REPEAT (SCROLL DOWN 500, `document.querySelector(".load-more")`)
|
||||
|
||||
# Alternative: Fixed number of scrolls
|
||||
REPEAT (SCROLL DOWN 800, 10)
|
||||
WAIT 2
|
||||
```
|
||||
|
||||
### Form Validation
|
||||
```c4a
|
||||
# Handle form with validation
|
||||
SET `#email` "invalid-email"
|
||||
CLICK `#submit`
|
||||
|
||||
# Check for validation error
|
||||
IF (EXISTS `.error-email`) THEN SET `#email` "valid@example.com"
|
||||
|
||||
# Retry submission
|
||||
CLICK `#submit`
|
||||
WAIT `.success-message` 5
|
||||
```
|
||||
|
||||
### Multi-step Process
|
||||
```c4a
|
||||
# Complex multi-step workflow
|
||||
PROC navigate_to_step
|
||||
CLICK `.next-button`
|
||||
WAIT `.step-content` 5
|
||||
ENDPROC
|
||||
|
||||
# Step 1
|
||||
WAIT `.step-1` 5
|
||||
SET `#name` "John Doe"
|
||||
navigate_to_step
|
||||
|
||||
# Step 2
|
||||
SET `#email` "john@example.com"
|
||||
navigate_to_step
|
||||
|
||||
# Step 3
|
||||
CLICK `#submit-final`
|
||||
WAIT `.confirmation` 10
|
||||
```
|
||||
|
||||
## Integration with Crawl4AI
|
||||
|
||||
Use C4A-Script with Crawl4AI for dynamic content interaction:
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
# Define interaction script
|
||||
script = """
|
||||
# Handle dynamic content loading
|
||||
WAIT `.content` 5
|
||||
IF (EXISTS `.load-more-button`) THEN CLICK `.load-more-button`
|
||||
WAIT `.additional-content` 5
|
||||
|
||||
# Accept cookies if needed
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-all`
|
||||
"""
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
c4a_script=script,
|
||||
wait_for=".content",
|
||||
screenshot=True
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com", config=config)
|
||||
print(result.markdown)
|
||||
```
|
||||
|
||||
This reference covers all available C4A-Script commands and patterns. For interactive learning, try the [tutorial](../examples/c4a_script/tutorial/) or [live demo](https://docs.crawl4ai.com/c4a-script/demo).
|
||||
395
docs/md_v2/core/c4a-script.md
Normal file
395
docs/md_v2/core/c4a-script.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# C4A-Script: Visual Web Automation Made Simple
|
||||
|
||||
## What is C4A-Script?
|
||||
|
||||
C4A-Script is a powerful, human-readable domain-specific language (DSL) designed for web automation and interaction. Think of it as a simplified programming language that anyone can read and write, perfect for automating repetitive web tasks, testing user interfaces, or creating interactive demos.
|
||||
|
||||
### Why C4A-Script?
|
||||
|
||||
**Simple Syntax, Powerful Results**
|
||||
```c4a
|
||||
# Navigate and interact in plain English
|
||||
GO https://example.com
|
||||
WAIT `#search-box` 5
|
||||
TYPE "Hello World"
|
||||
CLICK `button[type="submit"]`
|
||||
```
|
||||
|
||||
**Visual Programming Support**
|
||||
C4A-Script comes with a built-in Blockly visual editor, allowing you to create scripts by dragging and dropping blocks - no coding experience required!
|
||||
|
||||
**Perfect for:**
|
||||
- **UI Testing**: Automate user interaction flows
|
||||
- **Demo Creation**: Build interactive product demonstrations
|
||||
- **Data Entry**: Automate form filling and submissions
|
||||
- **Testing Workflows**: Validate complex user journeys
|
||||
- **Training**: Teach web automation without code complexity
|
||||
|
||||
## Getting Started: Your First Script
|
||||
|
||||
Let's create a simple script that searches for something on a website:
|
||||
|
||||
```c4a
|
||||
# My first C4A-Script
|
||||
GO https://duckduckgo.com
|
||||
|
||||
# Wait for the search box to appear
|
||||
WAIT `input[name="q"]` 10
|
||||
|
||||
# Type our search query
|
||||
TYPE "Crawl4AI"
|
||||
|
||||
# Press Enter to search
|
||||
PRESS Enter
|
||||
|
||||
# Wait for results
|
||||
WAIT `.results` 5
|
||||
```
|
||||
|
||||
That's it! In just a few lines, you've automated a complete search workflow.
|
||||
|
||||
## Interactive Tutorial & Live Demo
|
||||
|
||||
Want to learn by doing? We've got you covered:
|
||||
|
||||
**🚀 [Live Demo](https://docs.crawl4ai.com/c4a-script/demo)** - Try C4A-Script in your browser right now!
|
||||
|
||||
**📁 [Tutorial Examples](/examples/c4a_script/)** - Complete examples with source code
|
||||
|
||||
**🛠️ [Local Tutorial](/examples/c4a_script/tutorial/)** - Run the interactive tutorial on your machine
|
||||
|
||||
### Running the Tutorial Locally
|
||||
|
||||
The tutorial includes a Flask-based web interface with:
|
||||
- **Live Code Editor** with syntax highlighting
|
||||
- **Visual Blockly Editor** for drag-and-drop programming
|
||||
- **Recording Mode** to capture your actions and generate scripts
|
||||
- **Timeline View** to see and edit your automation steps
|
||||
|
||||
```bash
|
||||
# Clone and navigate to the tutorial
|
||||
cd docs/examples/c4a_script/tutorial/
|
||||
|
||||
# Install dependencies
|
||||
pip install flask
|
||||
|
||||
# Launch the tutorial server
|
||||
python app.py
|
||||
|
||||
# Open http://localhost:5000 in your browser
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Commands and Syntax
|
||||
|
||||
C4A-Script uses simple, English-like commands. Each command does one specific thing:
|
||||
|
||||
```c4a
|
||||
# Comments start with #
|
||||
COMMAND parameter1 parameter2
|
||||
|
||||
# Most commands use CSS selectors in backticks
|
||||
CLICK `#submit-button`
|
||||
|
||||
# Text content goes in quotes
|
||||
TYPE "Hello, World!"
|
||||
|
||||
# Numbers are used directly
|
||||
WAIT 3
|
||||
```
|
||||
|
||||
### Selectors: Finding Elements
|
||||
|
||||
C4A-Script uses CSS selectors to identify elements on the page:
|
||||
|
||||
```c4a
|
||||
# By ID
|
||||
CLICK `#login-button`
|
||||
|
||||
# By class
|
||||
CLICK `.submit-btn`
|
||||
|
||||
# By attribute
|
||||
CLICK `button[type="submit"]`
|
||||
|
||||
# By text content
|
||||
CLICK `button:contains("Sign In")`
|
||||
|
||||
# Complex selectors
|
||||
CLICK `.form-container input[name="email"]`
|
||||
```
|
||||
|
||||
### Variables and Dynamic Content
|
||||
|
||||
Store and reuse values with variables:
|
||||
|
||||
```c4a
|
||||
# Set a variable
|
||||
SETVAR username = "john@example.com"
|
||||
SETVAR password = "secret123"
|
||||
|
||||
# Use variables (prefix with $)
|
||||
TYPE $username
|
||||
PRESS Tab
|
||||
TYPE $password
|
||||
```
|
||||
|
||||
## Command Categories
|
||||
|
||||
### 🧭 Navigation Commands
|
||||
Move around the web like a user would:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `GO` | Navigate to URL | `GO https://example.com` |
|
||||
| `RELOAD` | Refresh current page | `RELOAD` |
|
||||
| `BACK` | Go back in history | `BACK` |
|
||||
| `FORWARD` | Go forward in history | `FORWARD` |
|
||||
|
||||
### ⏱️ Wait Commands
|
||||
Ensure elements are ready before interacting:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `WAIT` | Wait for time/element/text | `WAIT 3` or `WAIT \`#element\` 10` |
|
||||
|
||||
### 🖱️ Mouse Commands
|
||||
Click, drag, and move like a human:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `CLICK` | Click element or coordinates | `CLICK \`button\`` or `CLICK 100 200` |
|
||||
| `DOUBLE_CLICK` | Double-click element | `DOUBLE_CLICK \`.item\`` |
|
||||
| `RIGHT_CLICK` | Right-click element | `RIGHT_CLICK \`#menu\`` |
|
||||
| `SCROLL` | Scroll in direction | `SCROLL DOWN 500` |
|
||||
| `DRAG` | Drag from point to point | `DRAG 100 100 500 300` |
|
||||
|
||||
### ⌨️ Keyboard Commands
|
||||
Type text and press keys naturally:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `TYPE` | Type text or variable | `TYPE "Hello"` or `TYPE $username` |
|
||||
| `PRESS` | Press special keys | `PRESS Tab` or `PRESS Enter` |
|
||||
| `CLEAR` | Clear input field | `CLEAR \`#search\`` |
|
||||
| `SET` | Set input value directly | `SET \`#email\` "user@example.com"` |
|
||||
|
||||
### 🔀 Control Flow
|
||||
Add logic and repetition to your scripts:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `IF` | Conditional execution | `IF (EXISTS \`#popup\`) THEN CLICK \`#close\`` |
|
||||
| `REPEAT` | Loop commands | `REPEAT (SCROLL DOWN 300, 5)` |
|
||||
|
||||
### 💾 Variables & Advanced
|
||||
Store data and execute custom code:
|
||||
|
||||
| Command | Purpose | Example |
|
||||
|---------|---------|---------|
|
||||
| `SETVAR` | Create variable | `SETVAR email = "test@example.com"` |
|
||||
| `EVAL` | Execute JavaScript | `EVAL \`console.log('Hello')\`` |
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Example 1: Login Flow
|
||||
```c4a
|
||||
# Complete login automation
|
||||
GO https://myapp.com/login
|
||||
|
||||
# Wait for page to load
|
||||
WAIT `#login-form` 5
|
||||
|
||||
# Fill credentials
|
||||
CLICK `#email`
|
||||
TYPE "user@example.com"
|
||||
PRESS Tab
|
||||
TYPE "mypassword"
|
||||
|
||||
# Submit form
|
||||
CLICK `button[type="submit"]`
|
||||
|
||||
# Wait for dashboard
|
||||
WAIT `.dashboard` 10
|
||||
```
|
||||
|
||||
### Example 2: E-commerce Shopping
|
||||
```c4a
|
||||
# Shopping automation with variables
|
||||
SETVAR product = "laptop"
|
||||
SETVAR budget = "1000"
|
||||
|
||||
GO https://shop.example.com
|
||||
WAIT `#search-box` 3
|
||||
|
||||
# Search for product
|
||||
TYPE $product
|
||||
PRESS Enter
|
||||
WAIT `.product-list` 5
|
||||
|
||||
# Filter by price
|
||||
CLICK `.price-filter`
|
||||
SET `#max-price` $budget
|
||||
CLICK `.apply-filters`
|
||||
|
||||
# Select first result
|
||||
WAIT `.product-item` 3
|
||||
CLICK `.product-item:first-child`
|
||||
```
|
||||
|
||||
### Example 3: Form Automation with Conditions
|
||||
```c4a
|
||||
# Smart form filling with error handling
|
||||
GO https://forms.example.com
|
||||
|
||||
# Check if user is already logged in
|
||||
IF (EXISTS `.user-menu`) THEN GO https://forms.example.com/new
|
||||
IF (NOT EXISTS `.user-menu`) THEN CLICK `#login-link`
|
||||
|
||||
# Fill form
|
||||
WAIT `#contact-form` 5
|
||||
SET `#name` "John Doe"
|
||||
SET `#email` "john@example.com"
|
||||
SET `#message` "Hello from C4A-Script!"
|
||||
|
||||
# Handle popup if it appears
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-cookies`
|
||||
|
||||
# Submit
|
||||
CLICK `#submit-button`
|
||||
WAIT `.success-message` 10
|
||||
```
|
||||
|
||||
## Visual Programming with Blockly
|
||||
|
||||
C4A-Script includes a powerful visual programming interface built on Google Blockly. Perfect for:
|
||||
|
||||
- **Non-programmers** who want to create automation
|
||||
- **Rapid prototyping** of automation workflows
|
||||
- **Educational environments** for teaching automation concepts
|
||||
- **Collaborative development** where visual representation helps communication
|
||||
|
||||
### Features:
|
||||
- **Drag & Drop Interface**: Build scripts by connecting blocks
|
||||
- **Real-time Sync**: Changes in visual mode instantly update the text script
|
||||
- **Smart Block Types**: Blocks are categorized by function (Navigation, Actions, etc.)
|
||||
- **Error Prevention**: Visual connections prevent syntax errors
|
||||
- **Comment Support**: Add visual comment blocks for documentation
|
||||
|
||||
Try the visual editor in our [live demo](https://docs.crawl4ai.com/c4a-script/demo) or [local tutorial](/examples/c4a_script/tutorial/).
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Recording Mode
|
||||
The tutorial interface includes a recording feature that watches your browser interactions and automatically generates C4A-Script commands:
|
||||
|
||||
1. Click "Record" in the tutorial interface
|
||||
2. Perform actions in the browser preview
|
||||
3. Watch as C4A-Script commands are generated in real-time
|
||||
4. Edit and refine the generated script
|
||||
|
||||
### Error Handling and Debugging
|
||||
C4A-Script provides clear error messages and debugging information:
|
||||
|
||||
```c4a
|
||||
# Use comments for debugging
|
||||
# This will wait up to 10 seconds for the element
|
||||
WAIT `#slow-loading-element` 10
|
||||
|
||||
# Check if element exists before clicking
|
||||
IF (EXISTS `#optional-button`) THEN CLICK `#optional-button`
|
||||
|
||||
# Use EVAL for custom debugging
|
||||
EVAL `console.log("Current page title:", document.title)`
|
||||
```
|
||||
|
||||
### Integration with Crawl4AI
|
||||
C4A-Script integrates seamlessly with Crawl4AI's web crawling capabilities:
|
||||
|
||||
```python
|
||||
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
|
||||
|
||||
# Use C4A-Script for interaction before crawling
|
||||
script = """
|
||||
GO https://example.com
|
||||
CLICK `#load-more-content`
|
||||
WAIT `.dynamic-content` 5
|
||||
"""
|
||||
|
||||
config = CrawlerRunConfig(
|
||||
js_code=script,
|
||||
wait_for=".dynamic-content"
|
||||
)
|
||||
|
||||
async with AsyncWebCrawler() as crawler:
|
||||
result = await crawler.arun("https://example.com", config=config)
|
||||
print(result.markdown)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Wait for Elements
|
||||
```c4a
|
||||
# Bad: Clicking immediately
|
||||
CLICK `#button`
|
||||
|
||||
# Good: Wait for element to appear
|
||||
WAIT `#button` 5
|
||||
CLICK `#button`
|
||||
```
|
||||
|
||||
### 2. Use Descriptive Comments
|
||||
```c4a
|
||||
# Login to user account
|
||||
GO https://myapp.com/login
|
||||
WAIT `#login-form` 5
|
||||
|
||||
# Enter credentials
|
||||
TYPE "user@example.com"
|
||||
PRESS Tab
|
||||
TYPE "password123"
|
||||
|
||||
# Submit and wait for redirect
|
||||
CLICK `#submit-button`
|
||||
WAIT `.dashboard` 10
|
||||
```
|
||||
|
||||
### 3. Handle Variable Conditions
|
||||
```c4a
|
||||
# Handle different page states
|
||||
IF (EXISTS `.cookie-banner`) THEN CLICK `.accept-cookies`
|
||||
IF (EXISTS `.popup-modal`) THEN CLICK `.close-modal`
|
||||
|
||||
# Proceed with main workflow
|
||||
CLICK `#main-action`
|
||||
```
|
||||
|
||||
### 4. Use Variables for Reusability
|
||||
```c4a
|
||||
# Define once, use everywhere
|
||||
SETVAR base_url = "https://myapp.com"
|
||||
SETVAR test_email = "test@example.com"
|
||||
|
||||
GO $base_url/login
|
||||
SET `#email` $test_email
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **📖 [Complete Examples](/examples/c4a_script/)** - Real-world automation scripts
|
||||
- **🎮 [Interactive Tutorial](/examples/c4a_script/tutorial/)** - Hands-on learning environment
|
||||
- **📋 [API Reference](/api/c4a-script-reference/)** - Detailed command documentation
|
||||
- **🌐 [Live Demo](https://docs.crawl4ai.com/c4a-script/demo)** - Try it in your browser
|
||||
|
||||
## What's Next?
|
||||
|
||||
Ready to dive deeper? Check out:
|
||||
|
||||
1. **[API Reference](/api/c4a-script-reference/)** - Complete command documentation
|
||||
2. **[Tutorial Examples](/examples/c4a_script/)** - Copy-paste ready scripts
|
||||
3. **[Local Tutorial Setup](/examples/c4a_script/tutorial/)** - Run the full development environment
|
||||
|
||||
C4A-Script makes web automation accessible to everyone. Whether you're a developer automating tests, a designer creating interactive demos, or a business user streamlining repetitive tasks, C4A-Script has the tools you need.
|
||||
|
||||
*Start automating today - your future self will thank you!* 🚀
|
||||
@@ -22,6 +22,7 @@ nav:
|
||||
- "Command Line Interface": "core/cli.md"
|
||||
- "Simple Crawling": "core/simple-crawling.md"
|
||||
- "Deep Crawling": "core/deep-crawling.md"
|
||||
- "C4A-Script": "core/c4a-script.md"
|
||||
- "Crawler Result": "core/crawler-result.md"
|
||||
- "Browser, Crawler & LLM Config": "core/browser-crawler-config.md"
|
||||
- "Markdown Generation": "core/markdown-generation.md"
|
||||
@@ -55,6 +56,7 @@ nav:
|
||||
- "Browser, Crawler & LLM Config": "api/parameters.md"
|
||||
- "CrawlResult": "api/crawl-result.md"
|
||||
- "Strategies": "api/strategies.md"
|
||||
- "C4A-Script Reference": "api/c4a-script-reference.md"
|
||||
|
||||
theme:
|
||||
name: 'terminal'
|
||||
|
||||
Reference in New Issue
Block a user