PageSourceSearch

https://www.w3schools.com/tools/showcase.js?v=1.0

js w3schools.com collected 2026-09-24 08:24:32 UTC 60,014 bytes, 820 lines download raw bytes

1/* W3Schools Tools showcase — a rotating promo unit for the /tools/ catalogue.
2 *
3 * Usage (declarative — mounts itself on DOM ready):
4 *   <div data-ws-tools-showcase="hero"></div>
5 *   <script src="/tools/showcase.js"></script>
6 *
7 * Usage (programmatic — for slots built by other scripts):
8 *   WsToolsShowcase.mount(element, 'square');
9 *
10 * Formats:  hero  100% x 350   |  square  300x300
11 *           wide  160x300      |  sky     120x300
12 *           auto  picks square/sky/wide from the viewport, tracking the sidebar slot's
13 *                 990 / 1260 / 1700 breakpoints, and re-formats on resize.
14 *
15 * The TOOLS table below is GENERATED from the tool pages themselves — each entry is
16 * [slug, og:title, tool-hero-tagline] for every /tools/tool_*.php that has a matching
17 * screenshots/<slug>.webp. Delegating converter stubs (tool_png_to_jpg.php etc.) are
18 * excluded on purpose: their copy is templated by the engine .inc and they would
19 * crowd the rotation with near-identical entries. To regenerate, re-scan the tool
20 * pages for og:title + .tool-hero-tagline and rebuild the array.
21 */
22(function () {
23  var SHOTS = '/tools/screenshots/showcase/';
24  var FALLBACK = SHOTS + 'tool_meme_generator_square.png';
25  var LINK = '/tools/';
26  var INTERVAL = 7000;
27
28  var TOOLS = [
29    ["tool_aes_encrypt","AES Encrypt / Decrypt","Encrypt and decrypt text securely in your browser using AES-256-GCM with a password-derived key."],
30    ["tool_aspect_ratio","Aspect Ratio Calculator","Calculate and convert aspect ratios for images, video, and responsive design."],
31    ["tool_audio_converter","Audio to WAV Converter","Convert MP3, OGG, M4A, FLAC, and other audio files to uncompressed WAV format."],
32    ["tool_audio_cutter","Audio Cutter","Cut a section out of an audio file. Drag across the waveform to select the part to keep."],
33    ["tool_audio_info","Audio Metadata Viewer","View the duration, sample rate, channels, and estimated bitrate of an audio file."],
34    ["tool_audio_joiner","Audio Joiner","Join several audio files into one continuous track. Reorder them, then export as WAV."],
35    ["tool_audio_volume","Audio Volume & Fade","Make an audio file louder or quieter and add smooth fade-in and fade-out effects."],
36    ["tool_background_remover","Background Remover","Cut the background out of a photo automatically using an AI model that runs in your browser. Your image is never..."],
37    ["tool_barcode_generator","Barcode Generator","Create CODE128, EAN, UPC, CODE39 and ITF-14 barcodes and download them as SVG or PNG."],
38    ["tool_base32","Base32 Encode/Decode","Encode text to Base32 or decode Base32 back to text, using the standard RFC 4648 alphabet (A-Z, 2-7)."],
39    ["tool_base58","Base58 Encode/Decode","Encode text to Base58 or decode Base58 back to text, using the Bitcoin alphabet."],
40    ["tool_base64","Base64 Encode/Decode","Encode text to Base64 or decode Base64 to text."],
41    ["tool_bcrypt","Bcrypt Hash Generator","Generate and verify bcrypt password hashes."],
42    ["tool_binary_calculator","Binary and Bitwise Calculator","Enter two integers (decimal, or hexadecimal with a 0x prefix) to see the result of bitwise AND, OR, XOR, NOT, and shift..."],
43    ["tool_border_radius","Border Radius Generator","Create CSS border-radius values visually with independent corner control."],
44    ["tool_bulk_image_compress","Bulk Image Compressor","Compress many images together to a target file size or a fixed quality, then download them all."],
45    ["tool_case_converter","Case Converter","Convert text between different casing styles used in programming."],
46    ["tool_char_counter","Character Counter","Count characters, words, sentences, paragraphs, and lines in real time. Check your text against common social media and..."],
47    ["tool_chmod","Chmod Calculator","Calculate Unix file permissions in numeric and symbolic notation."],
48    ["tool_code_preview","Online Code Editors and Compilers","Write, edit, and run code directly in your browser."],
49    ["tool_color_blindness","Color Blindness Simulator","Upload an image to see how it looks to people with different types of color blindness."],
50    ["tool_color_converter","Color Converter","Convert colors between HEX, RGB, HSL, and HSV formats."],
51    ["tool_color_name","Color Name Finder","Enter a hex or RGB color to find the closest matching named CSS color."],
52    ["tool_color_palette","Color Palette Generator","Generate harmonious color palettes from a base color using color theory."],
53    ["tool_color_picker","Color Picker","This tool helps you pick colors and see how they look in different c
53ontexts."],
54    ["tool_color_shades","Shades and Tints Generator","Pick a base color and generate matching tints, shades, and tones. Click any swatch to copy its hex code."],
55    ["tool_contrast_checker","Color Contrast Checker","Check the contrast ratio between a text color and a background color and see if it passes WCAG accessibility guidelines."],
56    ["tool_crc32","CRC32 Checksum Calculator","Compute the CRC32 checksum of text or a file using the standard IEEE 802.3 polynomial. The result is shown as..."],
57    ["tool_cron","Cron Expression Parser","Parse cron expressions and see a human-readable explanation with next run times."],
58    ["tool_cron_builder","Cron Expression Builder","Build a cron expression with dropdowns and see a plain-English description plus the next run times."],
59    ["tool_css_animation","CSS Animation Generator","Create CSS keyframe animations visually with live preview."],
60    ["tool_css_blob","CSS Blob Generator","Generate organic blob shapes with the border-radius property and copy the code."],
61    ["tool_css_button","CSS Button Generator","Design custom CSS buttons visually and copy the code, including hover styles."],
62    ["tool_css_clip_path","CSS Clip Path Generator","Create CSS clip-path shapes visually with presets and custom polygon editing."],
63    ["tool_css_columns","CSS Multi-Column Generator","Create CSS multi-column layouts visually with live preview."],
64    ["tool_css_cubic_bezier","Cubic Bezier Editor","Drag the control points to design a custom easing curve and copy the code."],
65    ["tool_css_filter","CSS Filter Generator","Apply CSS filter effects to images with live preview."],
66    ["tool_css_flexbox","CSS Flexbox Playground","Experiment with Flexbox properties and see the results in real time."],
67    ["tool_css_formatter","CSS Formatter","Format (beautify) or minify CSS code."],
68    ["tool_css_glassmorphism","Glassmorphism Generator","Create frosted-glass UI effects visually and copy the CSS code."],
69    ["tool_css_gradient","CSS Gradient Generator","Create beautiful CSS gradients visually and copy the code."],
70    ["tool_css_grid","CSS Grid Generator","Create CSS Grid layouts visually and copy the code."],
71    ["tool_css_loader","CSS Loader Generator","Create animated CSS loaders visually and copy the HTML and CSS code."],
72    ["tool_css_minifier","CSS/JS Minifier","Minify CSS or JavaScript code to reduce file size."],
73    ["tool_css_neumorphism","Neumorphism Generator","Create soft-UI (neumorphic) elements visually and copy the CSS code."],
74    ["tool_css_shadow","CSS Box Shadow Generator","Create CSS box shadows visually and copy the code."],
75    ["tool_css_transform","CSS Transform Generator","Create CSS transform effects with live preview - rotate, scale, skew, translate, and 3D."],
76    ["tool_css_units","CSS Unit Converter","Convert between px, rem, em, pt, vw, and vh units."],
77    ["tool_csv_markdown","CSV to Markdown Table","Paste CSV data and convert it into a Markdown table. Supports quoted fields, custom delimiters, and column alignment."],
78    ["tool_csv_viewer","CSV Viewer","View CSV data as a formatted, sortable table."],
79    ["tool_curl_converter","cURL to Code Converter","Paste a curl command and generate equivalent JavaScript fetch() or Python requests code."],
80    ["tool_data_generator","Mock Data Generator","Generate realistic fake data for testing - names, emails, addresses, and more."],
81    ["tool_date_calculator","Date and Age Calculator","Calculate the difference between two dates, add or subtract time from a date, or work out an exact age."],
82    ["tool_demotivational_poster","Demotivational Poster Maker","Turn an image into the classic black-framed motivational-poster parody with a title and caption."],
83    ["tool_device_mockup","Device Mockup Generator","Upload a screenshot and place it inside a device frame. Download as PNG."],
84    ["tool_diff","Text Diff","Compare two texts and see the differences highlighted."],
85    ["tool_dockerfile_generator","Dockerfile Generator","Fill in the fields below to generate a starter Dockerfile for containerizing your application."],
86    ["tool_editorconfig_generator",".editorconfig Generator","Generate an .editorconfig file to keep coding styles consistent across editors and IDEs."],
87    ["tool_emoji_picker","Emoji Picker","Browse, search, and copy emojis. Click any emoji to copy it to clipboard."],
88    ["tool_encoding_detector","Text Encoding Detector","Analyze text encoding, view byte representation, and convert between encodings."],
89    ["tool_env_generator",".env Example Generator","Turn a real .env file into a safe .env.example, or build a new environment file from scratch."],
90    ["tool_escape","Escape / Unescape","Escape or unescape strings for use in JSON, HTML, URLs, SQL, and more."],
91    ["tool_exif_remover","Remove EXIF Data","Remove EXIF metadata - including GPS location - from a photo before sharing it."],
92    ["tool_favicon_generator","Favicon Generator","Turn any image into a complete favicon set - a multi-size favicon.ico plus PNG icons for modern browsers and mobile."],
93    ["tool_find_replace","Find and Replace","Search for text in your content and replace it with something else. Supports case-insensitive matching, whole-word..."],
94    ["tool_font_preview","Font Preview","Preview popular web-safe and Google Fonts with custom text and sizes."],
95    ["tool_gif_add_text","Add Text to GIF","Add a text caption to every frame of an animated GIF, then download the result."],
96    ["tool_gif_maker","GIF Maker","Turn a set of images into an animated GIF. Reorder the frames and set the speed, then download."],
97    ["tool_gif_speed","GIF Speed & Reverse","Speed up or slow down an animated GIF, play it in reverse, or both."],
98    ["tool_gif_to_frames","GIF to Frames","Split an animated GIF into its individual frames and download them as PNG images."],
99    ["tool_github_readme","README Generator","Generate a professional README.md file for your GitHub project."],
100    ["tool_gitignore_generator",".gitignore Generator","Select the technologies, languages, and tools used in your project to generate a combined .gitignore file."],
101    ["tool_hash","Hash Generator","Generate MD5, SHA-1, SHA-256, SHA-384, and SHA-512 hashes from text."],
102    ["tool_hmac_generator","HMAC Generator","Generate keyed-hash message authentication codes (HMAC) using SHA-1, SHA-256, SHA-384, or SHA-512."],
103    ["tool_hreflang_generator","Hreflang Tag Generator","Generate hreflang link tags to tell search engines which language and regional version of a page to serve."],
104    ["tool_htaccess",".htaccess Generator","Generate common .htaccess rules for Apache web servers."],
105    ["tool_html_entities","HTML Entities Encoder/Decoder","Encode special characters to HTML entities or decode HTML entities back to text."],
106    ["tool_html_formatter","HTML Formatter","Format (beautify) or minify HTML code."],
107    ["tool_html_markdown","HTML to Markdown Converter","Convert HTML to Markdown or Markdown to HTML, with live preview of the result."],
108    ["tool_http_headers","HTTP Header Analyzer","Paste raw HTTP response headers to see them parsed, explained, and checked for important security headers."],
109    ["tool_http_status","HTTP Status Codes","Searchable reference for all HTTP response status codes."],
110    ["tool_image_blur","Blur Image","Apply a Gaussian blur to an image. Adjust the radius for a soft or heavy blur."],
111    ["tool_image_border","Add Border to Image","Add a solid color border or frame around an image, with optional rounded corners."],
112    ["tool_image_circle_crop","Circle Crop Image","Crop an image into a circle with a transparent background. Drag to pan and use the slider to zoom."],
113    ["tool_image_color_picker","Image Color Picker","Upload an image and click anywhere on it to grab the exact color as HEX, RGB, and HSL."],
114    ["tool_image_compress","Compress Image","Reduce the file size of an image. Compress to a target size in KB or pick a quality level."],
115    ["tool_image_converter","Image Format Converter","Convert images between PNG, JPEG, WebP, and BMP formats."],
116    ["tool_image_crop","Crop Image","Crop an image to any size. Drag on the picture to select an area, then download the result."],
117    ["tool_image_exif","Image EXIF Metadata Viewer","Inspect the EXIF metadata stored in a photo - camera, lens, exposure, date, and GPS location."],
118    ["tool_image_flip","Flip Image","Mirror an image horizontally or vertically, then download the flipped result."],
119    ["tool_image_grayscale","Grayscale & Black and White Image","Convert a color photo to grayscale, pure black & white, or sepia tone."],
120    ["tool_image_increase_size","Increase Image Size in KB","Pad an image file up to a target size in KB 
120- useful for upload forms that require a minimum file size."],
121    ["tool_image_join","Join Images","Combine multiple images into one by stacking them horizontally or vertically."],
122    ["tool_image_palette","Color Palette from Image","Upload a photo and extract its dominant colors as a ready-to-use palette."],
123    ["tool_image_pixelate","Pixelate Image","Apply a mosaic / pixelation effect to an image. Great for censoring details or retro art."],
124    ["tool_image_resize","Resize Image","Resize an image by pixels, percentage, centimeters, millimeters, or inches."],
125    ["tool_image_rotate","Rotate Image","Rotate an image by 90&deg; steps or any custom angle, then download the result."],
126    ["tool_image_splitter","Image Splitter","Cut an image into an even grid of smaller pieces - great for Instagram grids and puzzles."],
127    ["tool_image_text","Add Text to Image","Add a custom text caption to an image. Drag the text on the preview to position it exactly."],
128    ["tool_image_to_pdf","Image to PDF Converter","Combine JPG, PNG, and WebP images into a single PDF document."],
129    ["tool_image_to_text","Image to Text Converter (OCR)","Use OCR (optical character recognition) to pull the text out of a photo, screenshot, or scanned document."],
130    ["tool_image_upscale","Image Upscaler","Upscale an image to 2&times;, 3&times;, or 4&times; its original size with smooth interpolation and optional sharpening."],
131    ["tool_image_watermark","Watermark Image","Add a text or logo watermark to an image. Place it in a corner or tile it across the whole picture."],
132    ["tool_js_formatter","JavaScript Formatter","Format (beautify) or minify JavaScript code."],
133    ["tool_json_compare","JSON Compare","Compare two JSON objects and see the differences highlighted."],
134    ["tool_json_csharp","JSON to C#","Generate C# classes from JSON data."],
135    ["tool_json_csv","JSON to CSV Converter","Convert between JSON arrays and CSV format."],
136    ["tool_json_formatter","JSON Formatter & Validator","Format, validate, and minify JSON data."],
137    ["tool_json_go","JSON to Go","Generate Go struct definitions from JSON data."],
138    ["tool_json_java","JSON to Java","Generate Java POJO classes from JSON data."],
139    ["tool_json_path","JSON Path Evaluator","Evaluate JSONPath expressions against JSON data."],
140    ["tool_json_python","JSON to Python","Generate Python dataclass definitions from JSON data."],
141    ["tool_json_schema","JSON Schema Generator","Generate a JSON Schema from sample JSON data."],
142    ["tool_json_tree","JSON Tree Viewer","Visualize JSON data as an interactive, collapsible tree with path navigation."],
143    ["tool_json_ts","JSON to TypeScript","Generate TypeScript interfaces from JSON data."],
144    ["tool_json_yaml","JSON to YAML Converter","Convert between JSON and YAML formats."],
145    ["tool_jsonld_schema","JSON-LD Schema Generator","Generate Schema.org JSON-LD structured data to help search engines understand your content and enable rich results."],
146    ["tool_jwt_decoder","JWT Decoder","Decode and inspect JSON Web Tokens."],
147    ["tool_jwt_signer","JWT Signer","Build and sign HMAC-based JSON Web Tokens (JWT) directly in your browser."],
148    ["tool_keypair_generator","Key Pair Generator","Generate RSA and Elliptic Curve key pairs in PEM format directly in your browser."],
149    ["tool_license_generator","License Generator","Generate a ready-to-use LICENSE file for your project from a popular open source license."],
150    ["tool_line_sort","Line Sort / Dedupe","Sort lines alphabetically, numerically, or randomly. Remove duplicates and empty lines."],
151    ["tool_lorem_ipsum","Lorem Ipsum Generator","Generate placeholder text for your designs and layouts."],
152    ["tool_markdown","Markdown Preview","Write Markdown and see a live preview side by side."],
153    ["tool_markdown_table","Markdown Table Generator","Build a Markdown table with an interactive grid editor, set column alignment, and copy the generated code."],
154    ["tool_meme_generator","Meme Generator","Add classic top and bottom captions to an image and download your meme."],
155    ["tool_meta_generator","' + esc(title) + '","Generate HTML meta tags for SEO and social media (Open Graph, Twitter Cards)."],
156    ["tool_mic_test","Microphone Test","Check that your microphone works: see a live volume meter and waveform."],
157    ["tool_mime_types","MIME Type Lookup","Searchable reference for common MIME types by file extension."],
158    ["tool_morse_code","Morse Code Translator","Convert plain text into International Morse code and decode Morse back into text. You can also play the Morse code as..."],
159    ["tool_mp4_to_mp3","MP4 to MP3 Converter","Extract the audio track from an MP4 (or MOV, WebM, MKV) video and save it as MP3 or WAV."],
160    ["tool_nato_phonetic","NATO Phonetic Alphabet","Spell out words and codes using the NATO phonetic alphabet, where each letter becomes a clear, unmistakable word like..."],
161    ["tool_notepad","Online Notepad","A simple notepad that automatically saves as you type."],
162    ["tool_number_base","Number Base Converter","Convert numbers between decimal, binary, octal, and hexadecimal."],
163    ["tool_open_graph_preview","' + esc(title) + '","Preview how your page will look when shared on Facebook, Twitter/X, and LinkedIn."],
164    ["tool_package_json","package.json Generator","Fill in the fields below to generate a valid package.json for your Node.js project."],
165    ["tool_password","Password Generator","Generate secure random passwords with customizable options."],
166    ["tool_password_strength","Password Strength Checker","Analyze the strength of a password as you type."],
167    ["tool_percentage_calculator","Percentage Calculator","Four percentage calculators in one: percent of a number, what percent X is of Y, percentage change, and increasing or..."],
168    ["tool_pixel_ruler","Pixel Ruler & Spacing Tool","Visualize pixel measurements, spacing scales, and common sizing references."],
169    ["tool_placeholder_img","Placeholder Image Generator","Generate placeholder images with custom size, color, and text using Canvas."],
170    ["tool_punycode","Punycode Converter","Convert internationalized domain names (IDN) to Punycode (xn-- form) and back, following RFC 3492."],
171    ["tool_qr_decoder","QR Code Scanner / Decoder","Decode a QR code from a picture, or scan one live with your camera."],
172    ["tool_qrcode","QR Code Generator","Generate QR codes for URLs, text, email, phone numbers, and more."],
173    ["tool_query_string","Query String Converter","Convert a URL query string into a JSON object and back. Accepts a full URL or a bare query string, decodes values, and..."],
174    ["tool_quoted_printable","Quoted-Printable Encode/Decode","Encode text to Quoted-Printable or decode it back, following the MIME rules from RFC 2045."],
175    ["tool_random_number","Random Number Generator","Generate cryptographically strong random integers in any range, roll dice, flip coins, or pick random items from a list."],
176    ["tool_regex","RegEx Tester","Test regular expressions against text with real-time matching and group capture."],
177    ["tool_responsive_test","Responsive Breakpoint Tester","Test how a URL looks at different screen widths using common device breakpoints."],
178    ["tool_robots_txt","robots.txt Generator","Generate a robots.txt file for your website to control search engine crawling."],
179    ["tool_roman_numerals","Roman Numeral Converter","Convert numbers (1 to 3999) to Roman numerals and Roman numerals back to numbers."],
180    ["tool_rot13","ROT13 and ROT47 Cipher","Apply the ROT13 or ROT47 cipher to your text. Both ciphers are their own inverse, so the same action both encodes and..."],
181    ["tool_signature_pad","Signature Maker","Draw a signature with your mouse, finger, or pen - or type it in a script font - then download it as an image."],
182    ["tool_sitemap_generator","Sitemap Generator","Paste a list of page URLs and generate a valid XML sitemap for search engines."],
183    ["tool_slug","Slug Generator","Convert text into URL-friendly slugs for web pages, blog posts, and APIs."],
184    ["tool_sql_formatter","SQL Formatter","Format (beautify) or minify SQL queries."],
185    ["tool_sql_to_json","SQL to JSON Converter","Convert SQL CREATE TABLE statements and INSERT queries to JSON Schema or JSON data."],
186    ["tool_string_inspector","String Inspector","Analyze text: character count, word count, line count, character frequency, and more."],
187    ["tool_subnet_calculator","IP Subnet Calculator","Enter an IPv4 address and CIDR prefix to calculate the network address, broadcast address, usable host range, and..."],
188    ["tool_svg_blob","SVG Blob Generator","Generate smooth, randomized blob shapes as SVG paths for backgrounds, avatars, and decorative design elements."],
189    ["tool_svg_optimizer","SVG Optimizer","Paste your SVG code below to remove comments, metadata, editor namespaces, and unnecessary whitespace."],
190    ["tool_svg_pattern","SVG Pattern Generator","Generate seamless, repeatable SVG background patterns and copy them as SVG code or a ready-to-use CSS data URI."],
191    ["tool_svg_to_datauri","SVG to Data URI","Turn SVG markup into a compact data URI you can embed directly in CSS or HTML with 
191no extra HTTP request."],
192    ["tool_svg_to_png","SVG to PNG Converter","Paste SVG code or upload an SVG file, choose a size, and download a rasterized PNG image."],
193    ["tool_svg_viewer","SVG Preview & Optimizer","Preview SVG code, view stats, and optimize by removing unnecessary attributes."],
194    ["tool_svg_wave","SVG Wave Generator","Generate smooth wave shapes in SVG to use as section dividers and decorative separators between page sections."],
195    ["tool_table_generator","HTML Table Generator","Generate HTML table code with custom rows, columns, and styling options."],
196    ["tool_text_binary","Text to Binary","Encode text into binary, hexadecimal, or decimal ASCII codes, and decode it back again. Choose a conversion mode and..."],
197    ["tool_text_shadow","CSS Text Shadow Generator","Create CSS text-shadow effects with live preview."],
198    ["tool_text_to_html","Text to HTML Converter","Convert plain text to HTML with automatic paragraph, link, and list detection."],
199    ["tool_text_to_speech","Text to Speech","Type or paste text and have your browser read it aloud. Choose a voice and adjust speed, pitch, and volume. Everything..."],
200    ["tool_timestamp","Unix Timestamp Converter","Convert between Unix timestamps and human-readable dates."],
201    ["tool_timezone_converter","Time Zone Converter","Convert a date and time between any two time zones, and see the same moment around the world."],
202    ["tool_token_generator","Token Generator","Generate cryptographically secure random tokens, API keys, PINs, and UUIDs in your browser."],
203    ["tool_unit_converter","Unit Converter","Convert between units of length, weight, temperature, data storage, area, and speed."],
204    ["tool_url_encoder","URL Encode/Decode","Encode special characters for use in URLs, or decode URL-encoded strings."],
205    ["tool_url_parser","URL Parser","Parse a URL into its components: protocol, host, port, path, query parameters, and fragment."],
206    ["tool_user_agent","User Agent Parser","Parse a user-agent string to identify the browser, operating system, rendering engine, and device type."],
207    ["tool_utm_builder","UTM Link Builder","Add UTM tracking parameters to any URL so you can measure marketing campaigns in your analytics."],
208    ["tool_uuid","UUID Generator","Generate random UUIDs (Universally Unique Identifiers) / GUIDs."],
209    ["tool_voice_recorder","Voice Recorder","Record your voice with the microphone, listen to it, and download it as an audio file."],
210    ["tool_webcam_test","Webcam Test","Check that your webcam works: see a live preview, resolution, and frame rate."],
211    ["tool_whitespace_cleaner","Whitespace Cleaner","Remove extra spaces, blank lines, tabs, and line breaks from text. Pick the operations you need and the cleaned result..."],
212    ["tool_word_counter","Word Counter","Count words, characters, sentences, paragraphs, and estimate reading time."],
213    ["tool_xml_formatter","XML Formatter","Format (beautify) or minify XML code. Also validates XML structure."],
214    ["tool_xml_json","XML to JSON Converter","Convert XML to JSON and JSON back to XML. Attributes are stored as \"@\" keys and text content as \"#text\"."],
215    ["tool_yaml_formatter","YAML Formatter & Validator","Validate YAML as you type, re-indent it with your preferred indentation, sort keys, and view it as JSON."]
216  ];
217
218  /* ── styles (injected once) ────────────────────────────────────────────── */
219  var CSS = [
220    // text-align is set explicitly: host pages like the homepage bands centre their
221    // content, and the unit must not inherit that
222    // text-align is set explicitly: host pages like the homepage bands centre their
223    // content, and the unit must not inherit that
224    '.wts{position:relative;display:block;overflow:hidden;box-sizing:border-box;text-align:left;',
225    'background:#fff;border:1px solid #ddd;border-radius:5px;text-decoration:none;color:#282A35;',
226    "font-family:'Source Sans Pro',sans-serif;transition:border-color .15s ease,box-shadow .15s ease}",
227    '.wts:hover{border-color:#04AA6D;box-shadow:0 2px 10px rgba(0,0,0,.08)}',
228    '.wts *{box-sizing:border-box}',
229    '.wts-shot{display:block;width:100%;background:#f1f1f1;overflow:hidden;position:relative;z-index:1}',
230    '.wts-shot img{display:block;width:100%;height:100%;object-fit:cover;object-position:top center}',
231    '.wts-body{display:flex;flex-direction:column;gap:4px;flex:0 0 auto;position:relative;z-index:1}',
232    /* the three column formats: screenshot absorbs whatever the copy does not use,
233       so a short tagline never leaves a gap and a long one never overflows */
234    '.wts-square,.wts-wide,.wts-sky{display:flex;flex-direction:column}',
235    /* 300x300 takes the wide capture, so its shot is 8:5 - the source lands
236       uncropped and the rest of the card is copy */
237    '.wts-square .wts-shot{flex:0 0 auto;height:auto;aspect-ratio:8/5}',
238    '.wts-square .wts-body{flex:1 1 auto;min-height:0}',
239    /* rail formats: the shot is exactly square, so the square capture lands 1:1 with
240       no side crop, and every pixel left over goes to the copy */
241    '.wts-wide .wts-shot,.wts-sky .wts-shot{flex:0 0 auto;height:auto;aspect-ratio:1/1}',
242    '.wts-wide .wts-body,.wts-sky .wts-body{flex:1 1 auto;min-height:0}',
243    '.wts-eyebrow{color:#04AA6D;font-weight:700;letter-spacing:.10em;text-transform:uppercase;line-height:1;align-self:flex-start}',
244    '.wts-title{font-weight:700;line-height:1.32;margin:0;padding-bottom:3px;flex:0 0 auto;',
245      'color:#282A35;overflow:hidden;'+
246      'display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}',
247    '.wts-desc{color:#757575;line-height:1.4;margin:0;padding-bottom:3px;overflow:hidden;',
248      'display:-webkit-box;-webkit-box-orient:vertical}',
249    '.wts-fade{transition:opacity .35s ease}',
250    '.wts-fade.wts-out{opacity:0}',
251
252    /* hero — full width x 350 */
253    '.wts-hero{width:100%;height:350px;display:flex;flex-direction:row-reverse}',
254    /* inset the capture so it reads as a framed screenshot instead of running flush
255       into the card's right border */
256    /* width is derived from the height, so the shot stays exactly 1:1 whatever the
257       card is wide and the square capture lands uncropped. Padding is symmetric or
258       the image inside the box would not be square. The copy column takes the rest. */
259    '.wts-hero .wts-shot{height:100%;aspect-ratio:1/1;width:auto;flex:none;background:#fff;padding:24px}',
260    '.wts-hero .wts-shot img{border:1px solid #e4e4e4;border-radius:6px}',
261    // landscape variant: 8:5 box for the 1200x750 capture, still uncropped
262    // 8:5 box for the 1200x750 capture. Sized from the WIDTH, not the height:
263    // driving it off the height made the art 557px wide and left the copy 178px.
264    '.wts-hero--wide .wts-shot{aspect-ratio:8/5;width:52%;height:auto;max-height:100%;align-self:center}',
265    '.wts-hero .wts-body{flex:1;justify-content:center;padding:32px;gap:10px}',
266    '.wts-hero .wts-eyebrow{font-size:13px}',
267    '.wts-hero .wts-title{font-size:30px}',
268    '.wts-hero .wts-desc{font-size:16px;-webkit-line-clamp:3;max-width:46ch}',
269
270    /* square — 300x300 */
271    '.wts-square{width:300px;height:300px;max-width:100%}',
272    // tool captures sat flush to the card edges and read as cramped - float them on
273    // a white mat instead. contain (not cover) so the inset never reintroduces a crop.
274    '.wts-square .wts-shot,.wts-wide .wts-shot,.wts-sky .wts-shot{background:#fff;padding:12px 12px 0}',
275    '.wts-square .wts-shot img,.wts-wide .wts-shot img,.wts-sky .wts-shot img{',
276      'object-fit:contain;object-position:top center;border:1px solid #eee;border-radius:4px}',
277    'body.darkpagetheme .wts-square .wts-shot,body.darkpagetheme .wts-wide .wts-shot,',
278      'body.darkpagetheme .wts-sky .wts-shot{background:#1d2a35}',
279    'body.darkpagetheme .wts-square .wts-shot img,body.darkpagetheme .wts-wide .wts-shot img,',
280      'body.darkpagetheme .wts-sky .wts-shot img{border-color:#3a4a5a}',
281    // Cap the art so a two-line heading plus the button always clear the card.
282    // The copy no longer flex-shrinks, so the art is what has to give.
283    '.wts-square .wts-shot{min-height:0;max-height:150px}',
284    '.wts-wide .wts-shot{max-height:130px}',
285    '.wts-sky .wts-shot{max-height:112px}',
286    '.wts-square .wts-body{padding:12px 14px;gap:5px}',
287    '.wts-square .wts-eyebrow{font-size:10px}',
288    '.wts-square .wts-title{font-size:17px}',
289    '.wts-square .wts-desc{font-size:13px;-webkit-line-clamp:2}',
290
291    /* wide skyscraper — 160x300 */
292    '.wts-wide{width:160px;max-width:100%;height:300px}',
293    '.wts-wide .wts-body{padding:18px 10px 10px;gap:4px}',
294    '.wts-wide .wts-eyebrow{font-size:9px}',
295    '.wts-wide .wts-title{font-size:14px}',
296    '.wts-wide .wts-desc{font-size:11.5px;-webkit-line-clamp:6}',
297
298    /* skyscraper — 120x300 */
299    '.wts-sky{width:120px;max-width:100%;height:300px}',
300    '.wts-sky .wts-body{padding:16px 8px 8px;gap:3px}',
301    '.wts-sky .wts-eyebrow{font-size:8px}',
302    '.wts-sky .wts-title{font-size:12.5px}',
303    '.wts-sky .wts-desc{font-size:10.5px;-webkit-line-clamp:7}',
304
305    /* ── promo variant ───────────────────────────────────────────────────────
306       Only the practice / projects slides take the share-card look (og-card.php):
307       #282A35 ground, green rule down the left, angled W3 mark. Tool slides keep
308       the light card. */
309    '.wts--promo{background:#282A35;border-color:#282A35;color:#fff}',
310    '.wts--promo:hover{border-color:#04AA6D;box-shadow:0 4px 16px rgba(0,0,0,.28)}',
311    '.wts--promo::before{content:"";position:absolute;left:0;top:0;bottom:0;background:#04AA6D;z-index:3}',
312    '.wts--promo::after{content:"";position:absolute;right:-5%;bottom:-10%;width:32%;height:44%;',
313      'background-color:#3a5a4a;transform:rotate(-18deg);z-index:0;pointer-events:none;',
314      '-webkit-mask:url(/safari-pinned-tab.svg) center/contain no-repeat;',
315      'mask:url(/safari-pinned-tab.svg) center/contain no-repeat}',
316    '.wts--promo.wts-hero::before{width:10px}',
317    '.wts--promo.wts-square::before{width:7px}',
318    '.wts--promo.wts-wide::before,.wts--promo.wts-sky::before{width:5px}',
319    /* kicker chip above the art, modelled on the practice hero's WEEKLY PROBLEM label */
320    '.wts-header{position:relative;z-index:2;align-self:flex-start;display:inline-block;',
321      'background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.10);color:#F59E0B;',
322      'font-weight:700;text-transform:uppercase;letter-spacing:.06em;border-radius:999px;white-space:nowrap}',
323    '.wts--promo.wts-square .wts-header,.wts--promo.wts-wide .wts-header,.wts--promo.wts-sky .wts-header{',
324      'margin:12px 12px 0}',
325    '.wts--promo.wts-square .wts-header{font-size:10px;padding:5px 10px}',
326    '.wts--promo.wts-wide .wts-header{font-size:9px;padding:4px 8px}',
327    '.wts--promo.wts-sky .wts-header{font-size:8px;padding:4px 7px;letter-spacing:.04em}',
328    /* the hero is a row, so float the chip over the top-left instead of in flow */
329    '.wts--promo.wts-hero .wts-header{position:absolute;top:22px;left:40px;font-size:11px;padding:5px 12px}',
330    '.wts--promo.wts-hero .wts-body{padding-top:56px}',
331    '.wts-meta{position:relative;z-index:2;color:#fff;font-weight:700;opacity:.85;white-space:nowrap}',
332    '.wts--promo.wts-square .wts-meta,.wts--promo.wts-wide .wts-meta,.wts--promo.wts-sky .wts-meta{',
333      'display:block;margin:8px 14px 0}',
334    '.wts--promo.wts-square .wts-meta{font-size:12px}',
335    '.wts--promo.wts-wide .wts-meta{font-size:11px}',
336    '.wts--promo.wts-sky .wts-meta{font-size:10px;margin:8px 8px 0}',
337    /* hero is a row, so lay it out as a grid: copy on the left spanning both rows,
338       art top-right with the countdown directly beneath it */
339    // rows: the art row flexes, the countdown row is content-sized. align-content
340    // cannot centre here because .wts-body spans both rows and sets their height,
341    // so the art centres within its own row instead.
342    '.wts--promo.wts-hero{display:grid;grid-template-columns:1fr 54%;grid-template-rows:1fr auto;',
343      'align-items:center}',
344    '.wts--promo.wts-hero .wts-body{grid-column:1;grid-row:1}',
345    // width:100% so the art fills its grid cell; width:auto sized it to the image's
346    // intrinsic width, which collapses to a quarter of the card before load
347    '.wts--promo.wts-hero .wts-shot{grid-column:2;grid-row:1;width:100%;height:auto;min-height:0;',
348      'aspect-ratio:auto;max-height:none;padding:0 30px 0 0;align-self:center}',
349    // fill the art column rather than sitting at the image's natural size, which
350    // rendered the code panel small against a large empty card
351    // cap the art so a tall capture (the project preview is 1000x816) still clears
352    // the card's 350px height once the grid has centred it
353    '.wts--promo.wts-hero .wts-shot img{width:100%;height:auto;max-height:246px;',
354      'object-fit:contain;object-position:center}',
355    '.wts--promo-practice.wts-hero .wts-shot img{max-height:210px}',
356    // hero only: the countdown reads better beneath the copy than under the art
357    '.wts--promo.wts-hero .wts-meta{grid-column:1;grid-row:2;font-size:18px;',
358      'margin:0 0 30px 40px;text-align:left;align-self:start}',
359    /* the promo card carries a kicker and a countdown on top of the usual copy, so the
360       art takes a fixed modest height and the copy clamps tighter than a tool slide */
361    '.wts--promo.wts-square .wts-shot{height:142px;min-height:0;aspect-ratio:auto;flex:0 0 auto}',
362    '.wts--promo.wts-wide .wts-shot{height:112px;min-height:0;aspect-ratio:auto;flex:0 0 auto}',
363    '.wts--promo.wts-sky .wts-shot{height:96px;min-height:0;aspect-ratio:auto;flex:0 0 auto}',
364    '.wts--promo.wts-square .wts-desc{-webkit-line-clamp:3}',
365    '.wts--promo.wts-wide .wts-desc{-webkit-line-clamp:4}',
366    '.wts--promo.wts-sky .wts-desc{-webkit-line-clamp:4}',
367    '.wts--promo.wts-hero .wts-title{font-size:28px}',
368    '.wts--promo.wts-hero .wts-desc{-webkit-line-clamp:2}',
369    '.wts--promo.wts-hero .wts-body{padding:48px 18px 10px 40px}',
370    /* the project card has no kicker chip and no countdown, so its art takes the
371       space those would have used */
372    '.wts--promo-project.wts-square .wts-shot{height:148px}',
373    '.wts--promo-project.wts-wide .wts-shot{height:110px}',
374    '.wts--promo-project.wts-sky .wts-shot{height:100px}',
375    '.wts--promo-project.wts-hero .wts-shot{padding:0 30px 0 0}',
376    '.wts--promo-project.wts-square .wts-desc{-webkit-line-clamp:2}',
377    '.wts--promo-project.wts-wide .wts-desc{-webkit-line-clamp:3}',
378    '.wts--promo-project.wts-sky .wts-desc{-webkit-line-clamp:3}',
379    '.wts--promo .wts-title{color:#fff}',
380    '.wts--promo .wts-desc{color:#aab4c0}',
381    '.wts--promo .wts-shot{background:transparent}',
382    /* the art is a panel on the card's own ground - contain it so it is never cropped
383       and never reads as a box inside a box */
384    // the tool mat's light border reads as a stray box on the promo card's own ground
385    '.wts--promo .wts-shot img{object-fit:contain;border:0;border-radius:6px}',
386    '.wts--promo.wts-hero .wts-shot img{border:0;border-radius:8px}',
387    '.wts--promo.wts-hero .wts-body{padding:34px 28px 34px 40px;
387gap:11px}',
388    '.wts--promo.wts-hero .wts-title{font-size:34px;letter-spacing:-.01em}',
389    '.wts--promo.wts-hero .wts-desc{max-width:52ch}',
390    '.wts--promo.wts-square .wts-shot,.wts--promo.wts-wide .wts-shot,.wts--promo.wts-sky .wts-shot{padding:14px 14px 0}',
391    'body.darkpagetheme .wts--promo{background:#222430;border-color:#222430}',
392    /* promo cards run less copy than a tool slide, so spend the spare room on padding */
393    '.wts--promo.wts-square .wts-body{padding:14px 18px 18px;gap:6px}',
394    '.wts--promo.wts-wide .wts-body{padding:12px 14px 16px;gap:5px}',
395    '.wts--promo.wts-sky .wts-body{padding:10px 12px 14px;gap:4px}',
396    '.wts--promo.wts-square .wts-shot,.wts--promo.wts-wide .wts-shot,.wts--promo.wts-sky .wts-shot{padding:16px 16px 0}',
397    '.wts--promo.wts-sky .wts-shot{padding:12px 11px 0}',
398    '.wts--promo.wts-square .wts-header{margin:14px 18px 0}',
399    '.wts--promo.wts-wide .wts-header{margin:12px 14px 0}',
400    '.wts--promo.wts-sky .wts-header{margin:10px 11px 0}',
401    '.wts--promo.wts-square .wts-meta{margin:10px 18px 0}',
402    '.wts--promo.wts-wide .wts-meta{margin:9px 14px 0}',
403    '.wts--promo.wts-sky .wts-meta{margin:8px 11px 0}',
404    /* ── project promo: the light mint surface from projects/inc_project_hero.inc,
405       not the dark share card, so it reads as the projects section it links to ── */
406    '.wts--promo-project{color:#282A35;border-color:#e5e7eb;',
407      'background:radial-gradient(circle at 18% 12%,rgba(4,170,109,.10),transparent 45%),',
408      'radial-gradient(circle at 92% 88%,rgba(4,170,109,.06),transparent 50%),',
409      'linear-gradient(180deg,#fbfdfc 0%,#f2f7f4 100%);background-color:#fbfdfc}',
410    '.wts--promo-project .wts-title{color:#282A35}',
411    '.wts--promo-project .wts-desc{color:#5a6470}',
412    '.wts--promo-project .wts-eyebrow{color:#04AA6D}',
413    '.wts--promo-project .wts-meta{color:#282A35}',
414    // the W3 mark reads as a smudge on a light ground - drop it to a faint mint
415    '.wts--promo-project::after{background-color:#D9EEE1;opacity:.85}',
416
417    /* ── narrow formats: heading + button only ────────────────────────────────
418       300x300 and the two skyscrapers drop the eyebrow and the description and end on a
419       button, so the card reads as image / heading / action. The hero keeps the fuller
420       layout, so the button and these overrides are scoped away from it. */
421    '.wts-square .wts-eyebrow,.wts-wide .wts-eyebrow,.wts-sky .wts-eyebrow,',
422      '.wts-square .wts-desc,.wts-wide .wts-desc,.wts-sky .wts-desc{display:none}',
423    '.wts-hero .wts-cta{display:none}',
424    /* W3Schools mark, like the login promo. Only the two skyscrapers show it - they
425       have the vertical room to spare and it puts the empty top strip to work. */
426    '.wts-logo{display:none}',
427    /* 300x300 tool cards set the mark beside the heading rather than above the art -
428       the square has the horizontal room the skyscrapers do not. Grid, so the pair can
429       centre as a unit while the button still spans the full width beneath. */
430    '.wts-logo-inline{display:none}',
431    /* optional sub-headline under the heading in the narrow formats (the hero has the
432       real description instead, so this never shows there) */
433    '.wts-sub{display:block;text-align:center;line-height:1.45;margin:0;flex:0 0 auto;color:#5a6470;',
434      'overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;',
435      'padding-bottom:3px}',
436    '.wts--promo .wts-sub{color:#aab4c0}',
437    '.wts--promo-project .wts-sub{color:#5a6470}',
438    '.wts-square .wts-sub{font-size:13.5px;margin-top:2px}',
439    '.wts-wide .wts-sub{font-size:11.5px;margin-top:2px}',
440    '.wts-sky .wts-sub{font-size:10.5px;margin-top:2px}',
441    '.wts-square:not(.wts--promo) .wts-logo-inline{display:block;line-height:0}',
442    '.wts-square:not(.wts--promo) .wts-logo-inline img{height:30px;width:auto;border:0;',
443      'border-radius:0;object-fit:contain}',
444    '.wts-square:not(.wts--promo) .wts-body{display:grid;grid-template-columns:auto minmax(0,1fr);',
445      'grid-template-rows:auto 1fr;column-gap:10px;align-items:center}',
446    '.wts-square:not(.wts--promo) .wts-logo-inline{grid-column:1;grid-row:1}',
447    '.wts-square:not(.wts--promo) .wts-title{grid-column:2;grid-row:1;text-align:left}',
448    '.wts-square:not(.wts--promo) .wts-cta{grid-column:1/3;grid-row:2;
448align-self:end}',
449    'body.darkpagetheme .wts-square .wts-logo-inline img{filter:brightness(0) invert(1);opacity:.92}',
450    '.wts-wide .wts-logo,.wts-sky .wts-logo{display:block;position:relative;z-index:2;text-align:center}',
451    '.wts-wide .wts-logo img,.wts-sky .wts-logo img{display:inline-block;width:auto;height:auto;',
452      'object-fit:contain;border:0;border-radius:0}',
453    '.wts-wide .wts-logo{margin:12px 0 2px}',
454    '.wts-wide .wts-logo img{height:26px}',
455    '.wts-sky .wts-logo{margin:10px 0 2px}',
456    '.wts-sky .wts-logo img{height:22px}',
457    // the mark is dark-on-light; invert it on the dark promo surface
458    '.wts--promo .wts-logo img{filter:brightness(0) invert(1);opacity:.92}',
459    '.wts--promo-project .wts-logo img{filter:none;opacity:1}',
460    'body.darkpagetheme .wts-wide .wts-logo img,body.darkpagetheme .wts-sky .wts-logo img{',
461      'filter:brightness(0) invert(1);opacity:.92}',
462    // practice moves its kicker down to become the heading, so hide the top chip here
463    '.wts-square .wts-header,.wts-wide .wts-header,.wts-sky .wts-header{display:none}',
464    // bigger headings now that the copy is gone
465    // headings centre with the button; the countdown sits in the same stack so it
466    // centres too, or it would read as misaligned between them
467    '.wts-square .wts-title,.wts-wide .wts-title,.wts-sky .wts-title,',
468      '.wts-square .wts-meta,.wts-wide .wts-meta,.wts-sky .wts-meta{text-align:center}',
469    '.wts-square .wts-title{font-size:21px;-webkit-line-clamp:2}',
470    '.wts-wide .wts-title{font-size:16px;-webkit-line-clamp:3}',
471    '.wts-sky .wts-title{font-size:13.5px;-webkit-line-clamp:3}',
472    // the body becomes heading -> (countdown) -> button, pinned to the bottom
473    '.wts-square .wts-body,.wts-wide .wts-body,.wts-sky .wts-body{justify-content:flex-start}',
474    // button modelled on the sign-in CTA: full width, solid green, bold
475    '.wts-cta{display:block;width:100%;box-sizing:border-box;margin-top:auto;text-align:center;',
476      'background:#04AA6D;color:#fff;font-weight:700;border-radius:5px;line-height:1.2;',
477      'white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
478    '.wts:hover .wts-cta{background:#059862}',
479    '.wts-square .wts-cta{font-size:15px;padding:11px 12px}',
480    '.wts-wide .wts-cta{font-size:12.5px;padding:9px 8px}',
481    '.wts-sky .wts-cta{font-size:11px;padding:8px 6px}',
482
483    /* dark mode — matches the tools template surfaces */
484    'body.darkpagetheme .wts{background:#1d2a35;border-color:#3a4a5a;color:#ddd}',
485    'body.darkpagetheme .wts-title{color:#ddd}',
486    'body.darkpagetheme .wts-desc{color:#aab}',
487    'body.darkpagetheme .wts-shot{background:#121f2b}',
488    /* the hero shot's padding is card-coloured, not shot-coloured, so the capture
489       floats on the card rather than sitting in a grey well */
490    'body.darkpagetheme .wts-hero .wts-shot{background:#1d2a35}',
491    'body.darkpagetheme .wts-hero .wts-shot img{border-color:#3a4a5a}',
492
493    /* hero stacks on narrow screens */
494    '@media (max-width:600px){',
495    '.wts-hero{flex-direction:column;height:auto}',
496    '.wts-hero .wts-shot{width:100%;height:170px;aspect-ratio:auto;padding:14px 14px 0}',
497    '.wts-hero .wts-body{padding:18px}',
498    '.wts-hero .wts-title{font-size:22px}',
499    '.wts-hero .wts-desc{font-size:14px;-webkit-line-clamp:2}',
500    '}',
501    '@media (prefers-reduced-motion:reduce){.wts-fade{transition:none}}'
502  ].join('');
503
504  function injectCss() {
505    if (document.getElementById('wts-css')) return;
506    var s = document.createElement('style');
507    s.id = 'wts-css';
508    s.appendChild(document.createTextNode(CSS));
509    (document.head || document.documentElement).appendChild(s);
510  }
511
512  /* ── rendering ─────────────────────────────────────────────────────────── */
513  function esc(s) {
514    return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
515      .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
516  }
517
518  var FORMATS = { hero: 1, square: 1, wide: 1, sky: 1 };
519
520  /* format:"auto" tracks the sidebar slot's existing <source media> breakpoints in
521     footer.inc / inc_footer.inc, so the unit keeps the rail's footprint at every width. */
522  var BREAKS = [[990, 'square'], [1260, 'sky'], [1700, 'wide']];
523  function autoFormat() {
524    var w = window.innerWidth || document.documentElement.clientWidth;
525    for (var i = 0; i < BREAKS.length; i++) if (w <= BREAKS[i][0]) return BREAKS[i][1];
526    return 'square';
527  }
528
529  /* Which capture each format wants, by the shape of its shot box:
530       hero    ~330x348 (1:1)   square      square  ~298x150 (2:1)   wide
531       wide    ~158x160 (1:1)   square      sky     ~118x160 (3:4)   square
532     The square captures are shot at a mobile viewport, so they are tall and full of
533     UI; the wide ones are the desktop layout. Picking the nearer aspect keeps
534     object-fit:cover from throwing away half the screenshot. */
535  var SHOT_FOR = { hero: 'square', square: 'wide', wide: 'square', sky: 'square' };
536
537  /* The hero is the one format whose width varies a lot - 735px on the showcase page,
538     980px in a homepage band, less in a narrow column. Above this width there is room
539     for the landscape capture; below it the square one keeps the copy readable. */
540  var HERO_WIDE_MIN = 700;
541  function shotFor(fmt, el) {
542    if (fmt !== 'hero') return SHOT_FOR[fmt] || 'wide';
543    var w = el ? el.getBoundingClientRect().width : 0;
544    if (!w && el && el.parentElement) w = el.parentElement.getBoundingClientRect().width;
545    return w >= HERO_WIDE_MIN ? 'wide' : 'square';
546  }
547
548  /* Tool entries are compact tuples; promo slides are objects. Normalise both to one
549     shape so the render path does not care which it is holding. */
550  function toSlide(t) {
551    if (t && t.href) return t;
552    return {
553      slug:  t[0],
554      title: t[1],
555      desc:  t[2],
556      href:  LINK + t[0] + '.php',
557      eyebrow: 'W3Schools Tools',
558      ga: 'ga-tool-' + t[0].replace(/^tool_/, '').replace(/_/g, '-')
559    };
560  }
561
562  function markup(slide, fmt, variant) {
563    var shot = SHOTS + slide.slug + '_' + (variant || SHOT_FOR[fmt] || 'wide');
564    // The narrow formats drop the eyebrow and the description and carry a button
565    // instead; the hero keeps the fuller layout.
566    var rail = (fmt !== 'hero');
567    var title = (rail && slide.railTitle) ? slide.railTitle : slide.title;
568    var cta = slide.cta || 'Open Tool';
569    // Only WebP is shipped per tool. The <img> needs some src for the handful of
570    // browsers without WebP, so every unit points at one shared PNG rather than
571    // doubling the asset count for a fallback almost nobody takes.
572    var webp = (rail && slide.railArt) ? slide.railArt : shot + '.webp';
573    var fallback = (rail && slide.railArtFallback) ? slide.railArtFallback : FALLBACK;
574    return '<span class="wts-logo" aria-hidden="true">' +
575             '<img src="/images/w3schools_logo_200.png" alt="" loading="lazy" decoding="async">' +
576           '</span>' +
577           (slide.header ? '<span class="wts-header">' + esc(slide.header) + '</span>' : '') +
578           '<span class="wts-shot">' +
579             '<picture>' +
580               '<source srcset="' + webp + '" type="image/webp">' +
581               '<img src="' + fallback + '" alt="" loading="lazy" decoding="async">' +
582             '</picture>' +
583           '</span>' +
584           (slide.meta ? '<span class="wts-meta">' + esc(slide.meta) + '</span>' : '') +
585           '<span class="wts-body">' +
586             '<span class="wts-eyebrow">' + esc(slide.eyebrow || 'W3Schools Tools') + '</span>' +
587             '<span class="wts-logo-inline" aria-hidden="true">' +
588               '<img src="/images/w3schools_logo_200.png" alt="" loading="lazy" decoding="async">' +
589             '</span>' +
590             '<span class="wts-title">' + esc(title) + '</span>' +
591             (rail && slide.railSub ? '<span class="wts-sub">' + esc(slide.railSub) + '</span>' : '') +
592             '<span class="wts-desc">' + esc(slide.desc) + '</span>' +
593             '<span class="wts-cta">' + esc(cta) + '</span>' +
594           '</span>';
595  }
596
597  function apply(el, tool, fmt) {
598    var slide = toSlide(tool);
599    el.href = slide.href;
600    el.target = '_blank';
601    el.rel = 'noopener';
602    el.setAttribute('aria-label', slide.title + ' - W3Schools (opens in a new tab)');
603    var variant = shotFor(fmt, el);
604    el.innerHTML = markup(slide, fmt, variant);
605    // A landscape capture needs a landscape shot box, or it would letterbox. Promos are
606    // exempt: they ship one art file for both variants and lay the hero out as a grid,
607    // so the wide rule's width:52% would fight the grid column.
608    el.classList.toggle('wts-hero--wide',
609      fmt === 'hero' && variant === 'wide' && !slide.promo);
610
611    /* Analytics. The hero is a featured placement; the fixed-size units live in the
612       right rail. The second class names what was clicked and is refreshed on every
613       rotation and on an auto re-format, so a click is attributed to what was shown. */
614    el.classList.remove('ga-featured', 'ga-right');
615    el.classList.add(fmt === 'hero' ? 'ga-featured' : 'ga-right');
616    for (var c = el.classList.length - 1; c >= 0; c--) {
617      var cl = el.classList[c];
618      if (cl.indexOf('ga-tool-') === 0 || cl.indexOf('ga-practice-') === 0 ||
619          cl.indexOf('ga-project-') === 0) el.classList.remove(cl);
620    }
621    el.classList.add(slide.ga);
622    // promo slides carry the share-card surface; tool slides keep the light card
623    el.classList.remove('wts--promo', 'wts--promo-practice', 'wts--promo-project');
624    if (slide.promo) {
625      el.classList.add('wts--promo');
626      if (slide.kind) el.classList.add('wts--promo-' + slide.kind);
627    }
628  }
629
630  /* ── promo slides ───────────────────────────────────────── */
631  /* Two non-tool slides. Which one a page gets is decided by its subject folder:
632     the front-end tutorials point at the beginner HTML project, everything else at
633     the weekly practice problem. */
634  var PROMOS = {
635    practice: {
636      shot: 'promo_practice',
637      header: 'Free Weekly Problem',
638      // the code panel reads well large in the hero but turns to noise at 120px wide,
639      // so the skyscrapers and the square get a device mockup instead
640      railArt: '/images/img_mockup.webp',
641      railArtFallback: '/images/img_mockup.png',
642      railTitle: 'Free Weekly Problem',
643      cta: 'Start Practicing',
644      eyebrow: 'W3Schools Practice',
645      title: 'Weekly Practice Problem',
646      desc: 'A new coding challenge every week. Solve it in your language of choice and earn XP.',
647      href: '/practice/index.php',
648      ga: 'ga-practice-weekly'
649    },
650    project: {
651      shot: 'promo_project',
652      railTitle: 'Build a Project',
653      railSub: 'Step-by-step projects in browser',
654      // the HTMLBIO001 browser preview is text-heavy and unreadable at rail widths;
655      // the game template reads as a project at a glance
656      railArt: '/projects/templates/GAME001/preview.webp',
657      railArtFallback: '/projects/templates/GAME001/preview.png',
658      cta: 'Start Project',
659      eyebrow: 'W3Schools Projects',
660      title: 'Build a Personal Page',
661      desc: 'Build your first HTML project, step-by-step. No setup, everything runs in your browser.',
662      href: '/projects/index.php',
663      ga: 'ga-project-htmlbio001'
664    }
665  };
666
667  var FRONTEND = { html: 1, css: 1, js: 1 };
668  function promoKindForPage() {
669    var seg = (location.pathname.split('/')[1] || '').toLowerCase();
670    return FRONTEND[seg] ? 'project' : 'practice';
671  }
672
673  /* The weekly problem rotates on a date schedule, so its name cannot live in this
674     file. Fetch it once per page and fill the card in when it lands; until then the
675     generic copy above stands, so the slide is never blank or wrong. */
676  var weeklyReq = null;
677  function withWeekly(cb) {
678    if (!weeklyReq) {
679      weeklyReq = fetch('/practice/weekly_json.php')
680        .then(function (r) { return r.json(); })
681        .catch(function () { return null; });
682    }
683    weeklyReq.then(cb);
684  }
685
686  function promoSlide(kind) {
687    var p = PROMOS[kind] || PROMOS.practice;
688    return { slug: p.shot, title: p.title, desc: p.desc, href: p.href,
689             eyebrow: p.eyebrow, header: p.header, railTitle: p.railTitle, cta: p.cta,
690             railArt: p.railArt, railArtFallback: p.railArtFallback, railSub: p.railSub,
691             ga: p.ga, promo: true, kind: kind };
692  }
693
694  /* ── mount ─────────────────────────────────────────────────────────────── */
695  function mount(host, fmt, opts) {
696    if (!host || !TOOLS.length) return null;
697    var auto = (fmt === 'auto');
698    fmt = auto ? autoFormat() : (FORMATS[fmt] ? fmt : 'square');
699    opts = opts || {};
700    injectCss();
701
702    var a = document.createElement('a');
703    // ga-featured / ga-right and the per-tool class are applied in apply(), which runs
704    // on every rotation - setting them here would go stale on the first swap
705    a.className = 'wts wts-' + fmt + ' wts-fade';
706    if (opts.gaClass) a.classList.add(opts.gaClass);
707
708    // Walk a shuffled order, not the table order - TOOLS is sorted by slug, so
709    // stepping i+1 marched through the catalogue alphabetically. Shuffling per mount
710    // also stops two units on the same page showing the same run of tools.
711    // Every tool appears once before any repeats.
712    var order = [], i;
713    for (i = 0; i < TOOLS.length; i++) order.push(i);
714    for (i = order.length - 1; i > 0; i--) {
715      var r = Math.floor(Math.random() * (i + 1)), t = order[i];
716      order[i] = order[r]; order[r] = t;
717    }
718    var p = 0;
719    // opts.slide pins the unit to one slide (promo mounts) instead of the catalogue
720    function current() { return opts.slide || TOOLS[order[p]]; }
721    apply(a, current(), fmt);
722
723    if (host.hasAttribute && host.hasAttribute('data-ws-tools-showcase')) {
724      host.innerHTML = '';
725      host.appendChild(a);
726    } else {
727      host.appendChild(a);
728    }
729    // width is only knowable once the unit is in the document, and the hero picks its
730    // capture from that - so resolve it again now it can be measured
731    if (fmt === 'hero') apply(a, current(), fmt);
732
733    var paused = false, timer = null;
734    var still = window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches;
735
736    function next() {
737      if (paused) return;
738      p = (p + 1) % order.length;
739      if (still) { apply(a, current(), fmt); return; }
740      a.classList.add('wts-out');
741      setTimeout(function () {
742        apply(a, current(), fmt);
743        a.classList.remove('wts-out');
744      }, 350);
745    }
746
747    if (TOOLS.length > 1 && opts.rotate !== false) {
748      timer = setInterval(next, opts.interval || INTERVAL);
749      // don't swap the link out from under someone reading or tabbing to it
750      a.addEventListener('mouseenter', function () { paused = true; });
751      a.addEventListener('mouseleave', function () { paused = false; });
752      a.addEventListener('focus', function () { paused = true; });
753      a.addEventListener('blur', function () { paused = false; });
754      document.addEventListener('visibilitychange', function () {
755        paused = document.hidden;
756      });
757    }
758
759    if (auto) {
760      window.addEventListener('resize', function () {
761        var f = autoFormat();
762        if (f === fmt) return;
763        a.classList.remove('wts-' + fmt);
764        fmt = f;
765        a.classList.add('wts-' + fmt);
766        apply(a, current(), fmt);   // re-stamps ga-featured/ga-right for the new format
767      });
768    }
769
770    return { el: a, fmt: function () { return fmt; },
771             stop: function () { if (timer) clearInterval(timer); } };
772  }
773
774  function init() {
775    var hosts = document.querySelectorAll('[data-ws-tools-showcase]');
776    for (var i = 0; i < hosts.length; i++) {
777      if (hosts[i].querySelector('.wts')) continue;
778      var want = hosts[i].getAttribute('data-ws-tools-showcase') || '';
779      var m = want.match(/^(hero|square|sky|wide|auto)-(practice|project|auto)$/);
780      if (m) mountPromo(hosts[i], m[2], m[1]);
781      else mount(hosts[i], want);
782    }
783  }
784
785  /* Mount a single promo slide (no rotation). kind is 'practice', 'project', or
786     'auto' to pick by subject folder. The practice card fills in the live problem
787     name once /practice/weekly_json.php answers. */
788  function mountPromo(host, kind, fmt, opts) {
789    if (kind === 'auto' || !kind) kind = promoKindForPage();
790    var slide = promoSlide(kind);
791    var unit = mount(host, fmt, mergeOpts(opts, { rotate: false, slide: slide }));
792    if (unit && kind === 'practice') {
793      withWeekly(function (w) {
794        if (!w || !w.ok || !w.name) return;
795        slide.title = w.name;
796        slide.desc = w.intro || slide.desc;
797        if (w.daysLeft != null) {
798          slide.meta = w.daysLeft + ' day' + (w.daysLeft === 1 ? '' : 's') + ' left';
799        }
800        apply(unit.el, slide, unit.fmt());
801      });
802    }
803    return unit;
804  }
805
806  function mergeOpts(o, extra) {
807    var out = {}, k;
808    for (k in (o || {})) if (Object.prototype.hasOwnProperty.call(o, k)) out[k] = o[k];
809    for (k in extra) out[k] = extra[k];
810    return out;
811  }
812
813  window.WsToolsShowcase = {
814    mount: mount, mountPromo: mountPromo, init: init,
815    tools: TOOLS, promoKindForPage: promoKindForPage
816  };
817
818  if (document.readyState !== 'loading') init();
819  else document.addEventListener('DOMContentLoaded', init);
820})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.