Spaces:
Sleeping
Sleeping
| """ | |
| WebCraft Class Explainer — Three-Tier Architecture | |
| =================================================== | |
| Deterministic, rule-based utility-class explainer for Tailwind v3 and | |
| Bootstrap 5. Requires no AI inference and no external dependencies beyond | |
| the Python standard library. | |
| Three-tier resolution strategy: | |
| Tier 1 — Exact dictionary lookup for high-frequency classes | |
| Tier 2 — Regex-based scale/pattern matching for numeric variants | |
| Tier 3 — Arbitrary value parser for Tailwind bracket syntax | |
| All explanations target a 12-year-old beginner reading their first | |
| real HTML/CSS code. Never raises exceptions on unknown input. | |
| """ | |
| import re | |
| # ===================================================================== | |
| # TIER 1 — Exact Dictionary Lookup | |
| # ===================================================================== | |
| _EXACT = { | |
| # --- Tailwind v3: Layout --- | |
| "flex": "use flexbox layout (children sit side by side)", | |
| "inline-flex": "inline flexbox (sits in text flow, children side by side)", | |
| "grid": "use grid layout (children arranged in rows and columns)", | |
| "inline-grid": "inline grid layout", | |
| "block": "block element (starts on a new line, takes full width)", | |
| "inline-block": "inline-block (sits in text flow but can have width/height)", | |
| "inline": "inline element (sits in text flow like a word)", | |
| "hidden": "hidden (element is invisible and takes no space)", | |
| "container": "centered container with max-width for each screen size", | |
| "table": "behaves like an HTML table", | |
| "contents": "element disappears from layout but children remain", | |
| # --- Tailwind v3: Flexbox helpers --- | |
| "flex-row": "flex children arranged left to right", | |
| "flex-row-reverse": "flex children arranged right to left", | |
| "flex-col": "flex children stacked top to bottom", | |
| "flex-col-reverse": "flex children stacked bottom to top", | |
| "flex-wrap": "flex children wrap to next line when they run out of room", | |
| "flex-nowrap": "flex children stay on one line (no wrapping)", | |
| "flex-wrap-reverse": "flex children wrap upward", | |
| "flex-1": "flex item grows and shrinks to fill available space", | |
| "flex-auto": "flex item grows and shrinks based on its content", | |
| "flex-initial": "flex item shrinks but does not grow", | |
| "flex-none": "flex item does not grow or shrink", | |
| "grow": "flex item grows to fill space", | |
| "grow-0": "flex item does not grow", | |
| "shrink": "flex item shrinks if needed", | |
| "shrink-0": "flex item does not shrink", | |
| "items-start": "align children to the top", | |
| "items-center": "align children to the vertical center", | |
| "items-end": "align children to the bottom", | |
| "items-baseline": "align children by their text baseline", | |
| "items-stretch": "stretch children to fill container height", | |
| "justify-start": "push children to the left", | |
| "justify-center": "center children horizontally", | |
| "justify-end": "push children to the right", | |
| "justify-between": "spread children with equal space between them", | |
| "justify-around": "spread children with equal space around them", | |
| "justify-evenly": "spread children with exactly equal gaps", | |
| "self-auto": "use parent's alignment for this item", | |
| "self-start": "align this item to the top", | |
| "self-center": "align this item to the center", | |
| "self-end": "align this item to the bottom", | |
| "self-stretch": "stretch this item to fill", | |
| # --- Tailwind v3: Grid helpers --- | |
| "grid-cols-1": "grid with 1 column", | |
| "grid-cols-2": "grid with 2 columns", | |
| "grid-cols-3": "grid with 3 columns", | |
| "grid-cols-4": "grid with 4 columns", | |
| "grid-cols-6": "grid with 6 columns", | |
| "grid-cols-12": "grid with 12 columns", | |
| "grid-rows-1": "grid with 1 row", | |
| "grid-rows-2": "grid with 2 rows", | |
| "grid-rows-3": "grid with 3 rows", | |
| "col-span-full": "spans all grid columns", | |
| "col-auto": "auto-sized grid column", | |
| "row-span-full": "spans all grid rows", | |
| "place-items-center": "center items both horizontally and vertically in grid", | |
| "place-content-center": "center all grid content", | |
| # --- Tailwind v3: Positioning --- | |
| "static": "default positioning (follows normal page flow)", | |
| "relative": "positioned relative to its normal spot", | |
| "absolute": "positioned relative to nearest positioned parent", | |
| "fixed": "stays in one spot on screen even when scrolling", | |
| "sticky": "sticks to the top when you scroll past it", | |
| "inset-0": "fills the entire parent (top, right, bottom, left all 0)", | |
| "top-0": "placed at the very top", | |
| "right-0": "placed at the very right", | |
| "bottom-0": "placed at the very bottom", | |
| "left-0": "placed at the very left", | |
| # --- Tailwind v3: Sizing --- | |
| "w-full": "takes up the full width", | |
| "w-screen": "as wide as the screen", | |
| "w-auto": "width adjusts to content", | |
| "w-fit": "width fits exactly around content", | |
| "w-min": "width shrinks to smallest possible", | |
| "w-max": "width grows to largest possible", | |
| "h-full": "takes up the full height of parent", | |
| "h-screen": "as tall as the screen", | |
| "h-auto": "height adjusts to content", | |
| "h-fit": "height fits exactly around content", | |
| "min-h-screen": "at least as tall as the screen", | |
| "min-h-full": "at least as tall as parent", | |
| "min-h-0": "minimum height reset to zero", | |
| "max-w-none": "no maximum width limit", | |
| "max-w-full": "maximum width is 100%", | |
| "max-w-screen-sm": "max width capped at small screen (640px)", | |
| "max-w-screen-md": "max width capped at medium screen (768px)", | |
| "max-w-screen-lg": "max width capped at large screen (1024px)", | |
| "max-w-screen-xl": "max width capped at extra large screen (1280px)", | |
| "max-w-xs": "max width 20rem (320px)", | |
| "max-w-sm": "max width 24rem (384px)", | |
| "max-w-md": "max width 28rem (448px)", | |
| "max-w-lg": "max width 32rem (512px)", | |
| "max-w-xl": "max width 36rem (576px)", | |
| "max-w-2xl": "max width 42rem (672px)", | |
| "max-w-3xl": "max width 48rem (768px)", | |
| "max-w-4xl": "max width 56rem (896px)", | |
| "max-w-5xl": "max width 64rem (1024px)", | |
| "max-w-6xl": "max width 72rem (1152px)", | |
| "max-w-7xl": "max width 80rem (1280px)", | |
| "max-w-prose": "max width sized for readable text (~65 characters)", | |
| # --- Tailwind v3: Typography --- | |
| "font-bold": "bold text", | |
| "font-semibold": "semi-bold text", | |
| "font-medium": "medium weight text", | |
| "font-normal": "normal weight text", | |
| "font-light": "light weight text", | |
| "font-thin": "thin text", | |
| "font-extrabold": "extra bold text", | |
| "font-black": "heaviest weight text", | |
| "font-sans": "sans-serif font (clean, modern look)", | |
| "font-serif": "serif font (classic, formal look)", | |
| "font-mono": "monospace font (code-style, fixed width)", | |
| "italic": "italic text", | |
| "not-italic": "remove italic style", | |
| "text-left": "text aligned to the left", | |
| "text-center": "text aligned to the center", | |
| "text-right": "text aligned to the right", | |
| "text-justify": "text stretched to fill the full width", | |
| "text-transparent": "text is invisible (used for gradient text effects)", | |
| "text-current": "text uses the current inherited color", | |
| "text-black": "black text", | |
| "text-white": "white text", | |
| "uppercase": "ALL LETTERS ARE UPPERCASE", | |
| "lowercase": "all letters are lowercase", | |
| "capitalize": "First Letter Of Each Word Is Capitalized", | |
| "normal-case": "normal text casing (no transform)", | |
| "truncate": "text cut off with ... when too long", | |
| "underline": "text has a line underneath", | |
| "line-through": "text has a line through the middle (strikethrough)", | |
| "no-underline": "remove underline from text", | |
| "leading-none": "no extra space between lines", | |
| "leading-tight": "lines close together", | |
| "leading-normal": "normal line spacing", | |
| "leading-relaxed": "extra space between lines", | |
| "leading-loose": "lots of space between lines", | |
| "tracking-tight": "letters squeezed closer together", | |
| "tracking-normal": "normal letter spacing", | |
| "tracking-wide": "letters spread apart", | |
| "tracking-wider": "letters spread further apart", | |
| "tracking-widest": "letters spread the furthest apart", | |
| "whitespace-nowrap": "text stays on one line (no wrapping)", | |
| "whitespace-normal": "text wraps normally", | |
| "break-words": "long words break to the next line", | |
| "break-all": "text breaks at any character", | |
| # --- Tailwind v3: Border & Shadow --- | |
| "border": "thin border around the element (1px)", | |
| "border-0": "no border", | |
| "border-2": "2px border", | |
| "border-4": "4px border", | |
| "border-8": "8px border", | |
| "border-t": "border on top edge only", | |
| "border-b": "border on bottom edge only", | |
| "border-l": "border on left edge only", | |
| "border-r": "border on right edge only", | |
| "border-solid": "solid line border", | |
| "border-dashed": "dashed line border", | |
| "border-dotted": "dotted line border", | |
| "border-none": "no border at all", | |
| "border-transparent": "border exists but is invisible", | |
| "border-black": "black border", | |
| "border-white": "white border", | |
| "rounded": "slightly rounded corners (4px)", | |
| "rounded-sm": "very slightly rounded corners (2px)", | |
| "rounded-md": "moderately rounded corners (6px)", | |
| "rounded-lg": "rounded corners (8px)", | |
| "rounded-xl": "more rounded corners (12px)", | |
| "rounded-2xl": "very rounded corners (16px)", | |
| "rounded-3xl": "extremely rounded corners (24px)", | |
| "rounded-full": "fully round (circle or pill shape)", | |
| "rounded-none": "sharp corners (no rounding)", | |
| "shadow": "small shadow behind the element", | |
| "shadow-sm": "tiny shadow", | |
| "shadow-md": "medium shadow", | |
| "shadow-lg": "large shadow", | |
| "shadow-xl": "extra large shadow", | |
| "shadow-2xl": "very large shadow", | |
| "shadow-none": "no shadow", | |
| "shadow-inner": "shadow inside the element", | |
| "ring-0": "no outline ring", | |
| "ring-1": "thin outline ring", | |
| "ring-2": "outline ring (2px)", | |
| "ring-4": "thick outline ring (4px)", | |
| # --- Tailwind v3: Background --- | |
| "bg-transparent": "transparent background", | |
| "bg-current": "background matches current text color", | |
| "bg-black": "black background", | |
| "bg-white": "white background", | |
| "bg-cover": "background image covers entire area", | |
| "bg-contain": "background image fits inside without cropping", | |
| "bg-center": "background image centered", | |
| "bg-no-repeat": "background image shown only once", | |
| "bg-repeat": "background image repeated to fill area", | |
| "bg-fixed": "background image stays still when scrolling", | |
| "bg-gradient-to-r": "gradient from left to right", | |
| "bg-gradient-to-l": "gradient from right to left", | |
| "bg-gradient-to-t": "gradient from bottom to top", | |
| "bg-gradient-to-b": "gradient from top to bottom", | |
| "bg-gradient-to-br": "gradient to bottom-right corner", | |
| "bg-gradient-to-bl": "gradient to bottom-left corner", | |
| "bg-gradient-to-tr": "gradient to top-right corner", | |
| "bg-gradient-to-tl": "gradient to top-left corner", | |
| "bg-clip-text": "background shows through text only (gradient text effect)", | |
| "bg-clip-border": "background extends to the border edge", | |
| "bg-clip-padding": "background extends to the padding edge", | |
| # --- Tailwind v3: Effects & Transitions --- | |
| "transition": "smooth animation when properties change", | |
| "transition-all": "animate all property changes smoothly", | |
| "transition-colors": "animate color changes smoothly", | |
| "transition-transform": "animate size/position changes smoothly", | |
| "transition-none": "no smooth animations", | |
| "ease-in": "animation starts slowly", | |
| "ease-out": "animation ends slowly", | |
| "ease-in-out": "animation starts and ends slowly", | |
| "animate-spin": "element spins continuously", | |
| "animate-ping": "element pulses like a radar ping", | |
| "animate-pulse": "element gently fades in and out", | |
| "animate-bounce": "element bounces up and down", | |
| # --- Tailwind v3: Interactivity --- | |
| "cursor-pointer": "mouse shows a pointing hand (clickable)", | |
| "cursor-default": "mouse shows normal arrow", | |
| "cursor-not-allowed": "mouse shows not-allowed symbol", | |
| "cursor-wait": "mouse shows loading spinner", | |
| "cursor-text": "mouse shows text cursor", | |
| "cursor-move": "mouse shows move arrows", | |
| "select-none": "text cannot be selected by user", | |
| "select-all": "clicking selects all text at once", | |
| "select-text": "text can be selected normally", | |
| "pointer-events-none": "element ignores all mouse clicks and hover", | |
| "pointer-events-auto": "element responds to mouse normally", | |
| "resize": "element can be resized by dragging its corner", | |
| "resize-none": "element cannot be resized", | |
| "resize-x": "element can be resized horizontally only", | |
| "resize-y": "element can be resized vertically only", | |
| # --- Tailwind v3: Overflow --- | |
| "overflow-hidden": "content that overflows is hidden (cut off)", | |
| "overflow-auto": "scrollbar appears only when content overflows", | |
| "overflow-scroll": "scrollbar always visible", | |
| "overflow-visible": "content can overflow outside the box", | |
| "overflow-x-auto": "horizontal scrollbar when needed", | |
| "overflow-x-hidden": "horizontal overflow hidden", | |
| "overflow-y-auto": "vertical scrollbar when needed", | |
| "overflow-y-hidden": "vertical overflow hidden", | |
| "overflow-y-scroll": "vertical scrollbar always visible", | |
| # --- Tailwind v3: Object Fit --- | |
| "object-cover": "image covers area (may crop edges)", | |
| "object-contain": "image fits inside area (may have gaps)", | |
| "object-fill": "image stretches to fill area", | |
| "object-none": "image shown at its natural size", | |
| "object-center": "image positioned at center", | |
| # --- Tailwind v3: Miscellaneous --- | |
| "sr-only": "visible only to screen readers (accessibility)", | |
| "not-sr-only": "visible to everyone (reverses sr-only)", | |
| "appearance-none": "remove browser default styling", | |
| "outline-none": "remove focus outline", | |
| "list-none": "remove bullet points from list", | |
| "list-disc": "bullet points on list items", | |
| "list-decimal": "numbered list items", | |
| "aspect-auto": "natural aspect ratio", | |
| "aspect-square": "square aspect ratio (1:1)", | |
| "aspect-video": "video aspect ratio (16:9)", | |
| "isolate": "creates a new stacking context", | |
| "isolation-auto": "default stacking behavior", | |
| # ===================================================================== | |
| # Bootstrap 5: Components | |
| # ===================================================================== | |
| "btn": "styled button", | |
| "btn-primary": "blue button (main action)", | |
| "btn-secondary": "gray button (secondary action)", | |
| "btn-success": "green button (positive action)", | |
| "btn-danger": "red button (warning/delete action)", | |
| "btn-warning": "yellow button (caution action)", | |
| "btn-info": "light blue button (information)", | |
| "btn-light": "light-colored button", | |
| "btn-dark": "dark-colored button", | |
| "btn-link": "button styled as a text link", | |
| "btn-outline-primary": "blue outlined button", | |
| "btn-outline-secondary": "gray outlined button", | |
| "btn-outline-success": "green outlined button", | |
| "btn-outline-danger": "red outlined button", | |
| "btn-outline-warning": "yellow outlined button", | |
| "btn-outline-info": "light blue outlined button", | |
| "btn-outline-light": "light outlined button", | |
| "btn-outline-dark": "dark outlined button", | |
| "btn-sm": "small button", | |
| "btn-lg": "large button", | |
| "navbar": "navigation bar component", | |
| "navbar-brand": "website name/logo in the navbar", | |
| "navbar-nav": "navigation links inside navbar", | |
| "navbar-toggler": "hamburger menu button for mobile", | |
| "navbar-expand-lg": "navbar shows full menu on large screens", | |
| "navbar-expand-md": "navbar shows full menu on medium screens", | |
| "navbar-expand-sm": "navbar shows full menu on small screens", | |
| "navbar-light": "navbar with light background", | |
| "navbar-dark": "navbar with dark background", | |
| "nav-link": "navigation link styled for navbar", | |
| "nav-item": "single item in navigation", | |
| "card": "card container (bordered box with padding)", | |
| "card-body": "content area inside a card", | |
| "card-title": "title text in a card", | |
| "card-text": "body text in a card", | |
| "card-header": "header area of a card", | |
| "card-footer": "footer area of a card", | |
| "card-img-top": "image at the top of a card", | |
| "modal": "popup dialog box (hidden by default)", | |
| "modal-dialog": "the dialog window inside a modal", | |
| "modal-content": "content wrapper inside modal", | |
| "modal-header": "header of the modal dialog", | |
| "modal-body": "body content of the modal", | |
| "modal-footer": "footer with buttons in the modal", | |
| "badge": "small label or counter", | |
| "alert": "notification message box", | |
| "alert-primary": "blue notification box", | |
| "alert-success": "green notification box", | |
| "alert-danger": "red notification box", | |
| "alert-warning": "yellow notification box", | |
| "alert-info": "light blue notification box", | |
| "form-control": "styled text input field", | |
| "form-label": "label text for a form field", | |
| "form-select": "styled dropdown select", | |
| "form-check": "checkbox or radio button group", | |
| "form-check-input": "the actual checkbox or radio", | |
| "form-check-label": "label for checkbox or radio", | |
| "input-group": "grouped input with add-ons", | |
| "input-group-text": "text add-on next to input field", | |
| "table": "styled data table", | |
| "table-striped": "table with alternating row colors", | |
| "table-hover": "table rows highlight on mouse hover", | |
| "table-bordered": "table with all borders visible", | |
| "carousel": "image slideshow component", | |
| "accordion": "collapsible content sections", | |
| "breadcrumb": "navigation trail showing current page location", | |
| "dropdown": "dropdown menu container", | |
| "dropdown-toggle": "button that opens a dropdown", | |
| "dropdown-menu": "the menu that drops down", | |
| "dropdown-item": "single option in a dropdown menu", | |
| "collapse": "content that can be shown or hidden", | |
| "tab-content": "content area that changes with tabs", | |
| "tab-pane": "single panel inside tab content", | |
| "spinner-border": "spinning loading indicator (circle)", | |
| "spinner-grow": "growing loading indicator (dot)", | |
| "progress": "progress bar container", | |
| "progress-bar": "the filled part of a progress bar", | |
| "toast": "brief notification popup", | |
| "offcanvas": "sidebar panel that slides in", | |
| # --- Bootstrap 5: Layout --- | |
| "row": "horizontal row in Bootstrap grid", | |
| "col": "auto-width column in grid", | |
| "container-fluid": "full-width container", | |
| "container-sm": "container fixed at small breakpoint", | |
| "container-md": "container fixed at medium breakpoint", | |
| "container-lg": "container fixed at large breakpoint", | |
| "container-xl": "container fixed at extra-large breakpoint", | |
| # --- Bootstrap 5: Display --- | |
| "d-flex": "use flexbox layout", | |
| "d-inline-flex": "inline flexbox layout", | |
| "d-block": "display as block element", | |
| "d-inline": "display as inline element", | |
| "d-inline-block": "display as inline-block", | |
| "d-none": "hidden (not displayed)", | |
| "d-grid": "display as grid", | |
| "d-table": "display as table", | |
| # --- Bootstrap 5: Flex utilities --- | |
| "flex-column": "flex children stacked top to bottom", | |
| "flex-fill": "flex item grows to fill space", | |
| "flex-grow-0": "flex item does not grow", | |
| "flex-grow-1": "flex item grows to fill space", | |
| "flex-shrink-0": "flex item does not shrink", | |
| "flex-shrink-1": "flex item can shrink", | |
| "align-items-start": "children aligned to top", | |
| "align-items-center": "children aligned to vertical center", | |
| "align-items-end": "children aligned to bottom", | |
| "align-items-stretch": "children stretched to fill height", | |
| "align-items-baseline": "children aligned by text baseline", | |
| "align-self-start": "this item aligned to top", | |
| "align-self-center": "this item aligned to center", | |
| "align-self-end": "this item aligned to bottom", | |
| "justify-content-start": "children pushed to the left", | |
| "justify-content-center": "children centered horizontally", | |
| "justify-content-end": "children pushed to the right", | |
| "justify-content-between": "equal space between children", | |
| "justify-content-around": "equal space around children", | |
| "justify-content-evenly": "exactly equal gaps between children", | |
| # --- Bootstrap 5: Text utilities --- | |
| "text-start": "text aligned to the left", | |
| "text-end": "text aligned to the right", | |
| "text-wrap": "text wraps to next line", | |
| "text-nowrap": "text stays on one line", | |
| "text-break": "long words break to avoid overflow", | |
| "text-lowercase": "all lowercase text", | |
| "text-uppercase": "ALL UPPERCASE TEXT", | |
| "text-capitalize": "First Letter Capitalized", | |
| "text-decoration-none": "no underline or strikethrough", | |
| "text-decoration-underline": "underlined text", | |
| "text-decoration-line-through": "strikethrough text", | |
| "text-muted": "gray/dimmed text color", | |
| "text-primary": "blue text (primary color)", | |
| "text-secondary": "gray text (secondary color)", | |
| "text-success": "green text", | |
| "text-danger": "red text", | |
| "text-warning": "yellow text", | |
| "text-info": "light blue text", | |
| "text-light": "light-colored text", | |
| "text-dark": "dark-colored text", | |
| "text-body": "default body text color", | |
| "text-black-50": "50% transparent black text", | |
| "text-white-50": "50% transparent white text", | |
| # --- Bootstrap 5: Other utilities --- | |
| "fw-bold": "bold text", | |
| "fw-bolder": "bolder text", | |
| "fw-semibold": "semi-bold text", | |
| "fw-normal": "normal weight text", | |
| "fw-light": "light weight text", | |
| "fw-lighter": "lighter weight text", | |
| "fst-italic": "italic text", | |
| "fst-normal": "normal (non-italic) text", | |
| "lead": "larger paragraph text for introductions", | |
| "small": "smaller text", | |
| "display-1": "very large display heading", | |
| "display-2": "large display heading", | |
| "display-3": "medium-large display heading", | |
| "display-4": "medium display heading", | |
| "display-5": "smaller display heading", | |
| "display-6": "smallest display heading", | |
| "w-25": "25% width", | |
| "w-50": "50% width", | |
| "w-75": "75% width", | |
| "w-100": "100% width (full)", | |
| "h-25": "25% height", | |
| "h-50": "50% height", | |
| "h-75": "75% height", | |
| "h-100": "100% height (full)", | |
| "mw-100": "max width 100%", | |
| "mh-100": "max height 100%", | |
| "vw-100": "100% of viewport width", | |
| "vh-100": "100% of viewport height", | |
| "min-vw-100": "at least 100% viewport width", | |
| "min-vh-100": "at least 100% viewport height", | |
| "visible": "element is visible", | |
| "invisible": "element is invisible but still takes space", | |
| "opacity-25": "25% visible (mostly transparent)", | |
| "opacity-50": "50% visible (half transparent)", | |
| "opacity-75": "75% visible (slightly transparent)", | |
| "opacity-100": "fully visible (not transparent)", | |
| "rounded-top": "top corners rounded", | |
| "rounded-bottom": "bottom corners rounded", | |
| "rounded-start": "left corners rounded", | |
| "rounded-end": "right corners rounded", | |
| "rounded-circle": "circular shape", | |
| "rounded-pill": "pill shape (very rounded sides)", | |
| "rounded-0": "no rounded corners", | |
| "rounded-1": "slightly rounded corners", | |
| "rounded-2": "moderately rounded corners", | |
| "rounded-3": "more rounded corners", | |
| "border-top": "border on top only", | |
| "border-bottom": "border on bottom only", | |
| "border-start": "border on left only", | |
| "border-end": "border on right only", | |
| "border-primary": "blue border", | |
| "border-secondary": "gray border", | |
| "border-success": "green border", | |
| "border-danger": "red border", | |
| "border-warning": "yellow border", | |
| "border-info": "light blue border", | |
| "border-light": "light-colored border", | |
| "border-dark": "dark-colored border", | |
| "border-1": "1px border", | |
| "border-3": "3px border", | |
| "border-5": "5px border", | |
| "shadow-sm": "small shadow", | |
| "shadow-lg": "large shadow", | |
| "shadow-none": "no shadow", | |
| "float-start": "float element to the left", | |
| "float-end": "float element to the right", | |
| "float-none": "no floating", | |
| "clearfix": "clears floated children", | |
| "position-static": "normal position in page flow", | |
| "position-relative": "positioned relative to normal spot", | |
| "position-absolute": "positioned relative to parent", | |
| "position-fixed": "stays in place when scrolling", | |
| "position-sticky": "sticks when scrolling past it", | |
| "top-50": "at the vertical center", | |
| "top-100": "at the very bottom", | |
| "start-0": "at the very left", | |
| "start-50": "at the horizontal center", | |
| "start-100": "at the very right", | |
| "end-0": "at the very right", | |
| "translate-middle": "centered using transform", | |
| "img-fluid": "responsive image (scales with container)", | |
| "img-thumbnail": "image with border frame", | |
| "ratio": "maintains aspect ratio", | |
| "ratio-1x1": "square aspect ratio", | |
| "ratio-4x3": "4:3 aspect ratio", | |
| "ratio-16x9": "16:9 widescreen ratio", | |
| "ratio-21x9": "21:9 ultra-wide ratio", | |
| "stretched-link": "stretches click area to fill parent", | |
| "text-truncate": "text cut off with ... when too long", | |
| "visually-hidden": "hidden from screen but visible to screen readers", | |
| "fixed-top": "fixed to top of screen", | |
| "fixed-bottom": "fixed to bottom of screen", | |
| "sticky-top": "sticks to top when scrolling", | |
| "pe-none": "ignores mouse clicks", | |
| "pe-auto": "responds to mouse normally", | |
| "user-select-all": "clicking selects all text", | |
| "user-select-auto": "normal text selection", | |
| "user-select-none": "text cannot be selected", | |
| "mx-auto": "horizontally centered with auto margins", | |
| } | |
| # ===================================================================== | |
| # TIER 2 — Regex-Based Scale Pattern Matching | |
| # ===================================================================== | |
| _TAILWIND_TEXT_SIZES = { | |
| "xs": "0.75rem (12px)", "sm": "0.875rem (14px)", "base": "1rem (16px)", | |
| "lg": "1.125rem (18px)", "xl": "1.25rem (20px)", "2xl": "1.5rem (24px)", | |
| "3xl": "1.875rem (30px)", "4xl": "2.25rem (36px)", "5xl": "3rem (48px)", | |
| "6xl": "3.75rem (60px)", "7xl": "4.5rem (72px)", "8xl": "6rem (96px)", | |
| "9xl": "8rem (128px)", | |
| } | |
| _SPACING_DIRS = { | |
| "": "", "x": "horizontal ", "y": "vertical ", | |
| "t": "top ", "b": "bottom ", "l": "left ", "r": "right ", | |
| "s": "start (left) ", "e": "end (right) ", | |
| } | |
| _BOOTSTRAP_SPACING_PX = { | |
| "0": "0px", "1": "4px", "2": "8px", "3": "16px", "4": "24px", "5": "48px", | |
| } | |
| _BS_BREAKPOINT_PX = { | |
| "sm": "576px", "md": "768px", "lg": "992px", | |
| "xl": "1200px", "xxl": "1400px", | |
| } | |
| _RE_TW_PADDING = re.compile(r'^p([xytrbl]?)-(\d+|auto)$') | |
| _RE_TW_MARGIN = re.compile(r'^-?m([xytrbl]?)-(\d+|auto)$') | |
| _RE_TW_GAP = re.compile(r'^gap-?([xy])?-?(\d+)$') | |
| _RE_TW_WIDTH_FRAC = re.compile(r'^w-(\d+)/(\d+)$') | |
| _RE_TW_HEIGHT_FRAC = re.compile(r'^h-(\d+)/(\d+)$') | |
| _RE_TW_WIDTH = re.compile(r'^w-(\d+)$') | |
| _RE_TW_HEIGHT = re.compile(r'^h-(\d+)$') | |
| _RE_TW_TEXT_SIZE = re.compile(r'^text-(xs|sm|base|lg|xl|[2-9]xl)$') | |
| _RE_TW_TEXT_COLOR = re.compile(r'^text-([a-z]+)-(\d{2,3})$') | |
| _RE_TW_BG_COLOR = re.compile(r'^bg-([a-z]+)-(\d{2,3})$') | |
| _RE_TW_BORDER_WIDTH = re.compile(r'^border-(\d+)$') | |
| _RE_TW_BORDER_COLOR = re.compile(r'^border-([a-z]+)-(\d{2,3})$') | |
| _RE_TW_Z = re.compile(r'^z-(\d+)$') | |
| _RE_TW_OPACITY = re.compile(r'^opacity-(\d+)$') | |
| _RE_TW_SPACE_X = re.compile(r'^space-x-(\d+)$') | |
| _RE_TW_SPACE_Y = re.compile(r'^space-y-(\d+)$') | |
| _RE_TW_DURATION = re.compile(r'^duration-(\d+)$') | |
| _RE_TW_DELAY = re.compile(r'^delay-(\d+)$') | |
| _RE_TW_SCALE = re.compile(r'^scale-(\d+)$') | |
| _RE_TW_GRID_COLS = re.compile(r'^grid-cols-(\d+)$') | |
| _RE_TW_GRID_ROWS = re.compile(r'^grid-rows-(\d+)$') | |
| _RE_TW_COL_SPAN = re.compile(r'^col-span-(\d+)$') | |
| _RE_TW_ROW_SPAN = re.compile(r'^row-span-(\d+)$') | |
| _RE_TW_GRADIENT_FROM = re.compile(r'^from-([a-z]+)-(\d{2,3})$') | |
| _RE_TW_GRADIENT_TO = re.compile(r'^to-([a-z]+)-(\d{2,3})$') | |
| _RE_TW_GRADIENT_VIA = re.compile(r'^via-([a-z]+)-(\d{2,3})$') | |
| _RE_TW_INSET = re.compile(r'^inset-(\d+)$') | |
| _RE_TW_TOP = re.compile(r'^top-(\d+)$') | |
| _RE_TW_BOTTOM = re.compile(r'^bottom-(\d+)$') | |
| _RE_TW_LEFT = re.compile(r'^left-(\d+)$') | |
| _RE_TW_RIGHT = re.compile(r'^right-(\d+)$') | |
| _RE_BS_COL = re.compile(r'^col-(sm|md|lg|xl|xxl)-(\d{1,2}|auto)$') | |
| _RE_BS_COL_PLAIN = re.compile(r'^col-(\d{1,2})$') | |
| _RE_BS_SPACING = re.compile(r'^([mp])([xytrblse]?)-([0-5]|auto)$') | |
| _RE_BS_DISPLAY_BP = re.compile( | |
| r'^d-(sm|md|lg|xl|xxl)-(none|block|flex|inline|inline-block|grid|table)$' | |
| ) | |
| _RE_BS_ORDER = re.compile(r'^order-(\d+|first|last)$') | |
| _RE_BS_GUTTER = re.compile(r'^g([xy]?)-([0-5])$') | |
| def _try_tier2(cls: str) -> str | None: | |
| """Attempt regex-based scale matching. Returns explanation or None.""" | |
| m = _RE_TW_PADDING.match(cls) | |
| if m: | |
| direction, val = m.groups() | |
| d = _SPACING_DIRS.get(direction, "") | |
| if val == "auto": | |
| return f"{d}padding set to auto" | |
| rem = int(val) * 0.25 | |
| return f"{d}padding {rem}rem ({int(rem * 16)}px)" | |
| neg = cls.startswith("-") | |
| m = _RE_TW_MARGIN.match(cls) | |
| if m: | |
| direction, val = m.groups() | |
| d = _SPACING_DIRS.get(direction, "") | |
| if val == "auto": | |
| return f"{d}margin set to auto (centers element)" | |
| rem = int(val) * 0.25 | |
| sign = "negative " if neg else "" | |
| return f"{sign}{d}margin {rem}rem ({int(rem * 16)}px)" | |
| m = _RE_TW_GAP.match(cls) | |
| if m: | |
| axis, val = m.groups() | |
| d = {"x": "horizontal ", "y": "vertical "}.get(axis or "", "") | |
| rem = int(val) * 0.25 | |
| return f"{d}gap between children {rem}rem ({int(rem * 16)}px)" | |
| m = _RE_TW_WIDTH_FRAC.match(cls) | |
| if m: | |
| n, d = int(m.group(1)), int(m.group(2)) | |
| pct = round(n / d * 100, 1) | |
| return f"width {n}/{d} ({pct}%)" | |
| m = _RE_TW_HEIGHT_FRAC.match(cls) | |
| if m: | |
| n, d = int(m.group(1)), int(m.group(2)) | |
| pct = round(n / d * 100, 1) | |
| return f"height {n}/{d} ({pct}%)" | |
| m = _RE_TW_WIDTH.match(cls) | |
| if m: | |
| val = int(m.group(1)) | |
| rem = val * 0.25 | |
| return f"width {rem}rem ({int(rem * 16)}px)" | |
| m = _RE_TW_HEIGHT.match(cls) | |
| if m: | |
| val = int(m.group(1)) | |
| rem = val * 0.25 | |
| return f"height {rem}rem ({int(rem * 16)}px)" | |
| m = _RE_TW_TEXT_SIZE.match(cls) | |
| if m: | |
| size = m.group(1) | |
| rem_val = _TAILWIND_TEXT_SIZES.get(size, size) | |
| return f"font size {size} ({rem_val})" | |
| m = _RE_TW_TEXT_COLOR.match(cls) | |
| if m: | |
| return f"text color {m.group(1)} shade {m.group(2)}" | |
| m = _RE_TW_BG_COLOR.match(cls) | |
| if m: | |
| return f"background color {m.group(1)} shade {m.group(2)}" | |
| m = _RE_TW_BORDER_WIDTH.match(cls) | |
| if m: | |
| return f"border width {m.group(1)}px" | |
| m = _RE_TW_BORDER_COLOR.match(cls) | |
| if m: | |
| return f"border color {m.group(1)} shade {m.group(2)}" | |
| m = _RE_TW_Z.match(cls) | |
| if m: | |
| return f"z-index (stacking order) {m.group(1)}" | |
| m = _RE_TW_OPACITY.match(cls) | |
| if m: | |
| return f"opacity {m.group(1)}%" | |
| m = _RE_TW_SPACE_X.match(cls) | |
| if m: | |
| val = int(m.group(1)) | |
| rem = val * 0.25 | |
| return f"horizontal space between children {rem}rem ({int(rem * 16)}px)" | |
| m = _RE_TW_SPACE_Y.match(cls) | |
| if m: | |
| val = int(m.group(1)) | |
| rem = val * 0.25 | |
| return f"vertical space between children {rem}rem ({int(rem * 16)}px)" | |
| m = _RE_TW_DURATION.match(cls) | |
| if m: | |
| return f"animation duration {m.group(1)}ms" | |
| m = _RE_TW_DELAY.match(cls) | |
| if m: | |
| return f"animation delay {m.group(1)}ms" | |
| m = _RE_TW_SCALE.match(cls) | |
| if m: | |
| return f"scale element to {m.group(1)}%" | |
| m = _RE_TW_GRID_COLS.match(cls) | |
| if m: | |
| return f"grid with {m.group(1)} columns" | |
| m = _RE_TW_GRID_ROWS.match(cls) | |
| if m: | |
| return f"grid with {m.group(1)} rows" | |
| m = _RE_TW_COL_SPAN.match(cls) | |
| if m: | |
| return f"spans {m.group(1)} grid columns" | |
| m = _RE_TW_ROW_SPAN.match(cls) | |
| if m: | |
| return f"spans {m.group(1)} grid rows" | |
| m = _RE_TW_GRADIENT_FROM.match(cls) | |
| if m: | |
| return f"gradient starts with {m.group(1)} shade {m.group(2)}" | |
| m = _RE_TW_GRADIENT_TO.match(cls) | |
| if m: | |
| return f"gradient ends with {m.group(1)} shade {m.group(2)}" | |
| m = _RE_TW_GRADIENT_VIA.match(cls) | |
| if m: | |
| return f"gradient passes through {m.group(1)} shade {m.group(2)}" | |
| for regex, label in [ | |
| (_RE_TW_INSET, "inset (all sides)"), | |
| (_RE_TW_TOP, "top offset"), | |
| (_RE_TW_BOTTOM, "bottom offset"), | |
| (_RE_TW_LEFT, "left offset"), | |
| (_RE_TW_RIGHT, "right offset"), | |
| ]: | |
| m = regex.match(cls) | |
| if m: | |
| val = int(m.group(1)) | |
| rem = val * 0.25 | |
| return f"{label} {rem}rem ({int(rem * 16)}px)" | |
| m = _RE_BS_COL.match(cls) | |
| if m: | |
| bp, cols = m.groups() | |
| bp_px = _BS_BREAKPOINT_PX.get(bp, bp) | |
| if cols == "auto": | |
| return f"auto-width column on {bp} screens ({bp_px}) and up" | |
| return f"{cols} of 12 columns on {bp} screens ({bp_px}) and up" | |
| m = _RE_BS_COL_PLAIN.match(cls) | |
| if m: | |
| return f"{m.group(1)} of 12 columns wide" | |
| m = _RE_BS_SPACING.match(cls) | |
| if m: | |
| prop, axis, val = m.groups() | |
| prop_word = "margin" if prop == "m" else "padding" | |
| d = _SPACING_DIRS.get(axis, "") | |
| if val == "auto": | |
| return f"{d}{prop_word} set to auto" | |
| px = _BOOTSTRAP_SPACING_PX.get(val, val) | |
| return f"{d}{prop_word} level {val} ({px})" | |
| m = _RE_BS_DISPLAY_BP.match(cls) | |
| if m: | |
| bp, disp = m.groups() | |
| bp_px = _BS_BREAKPOINT_PX.get(bp, bp) | |
| disp_human = "hidden" if disp == "none" else f"displayed as {disp}" | |
| return f"{disp_human} on {bp} screens ({bp_px}) and up" | |
| m = _RE_BS_ORDER.match(cls) | |
| if m: | |
| return f"visual order set to {m.group(1)}" | |
| m = _RE_BS_GUTTER.match(cls) | |
| if m: | |
| axis, val = m.groups() | |
| d = {"x": "horizontal ", "y": "vertical "}.get(axis or "", "") | |
| return f"{d}gutter spacing level {val}" | |
| return None | |
| # ===================================================================== | |
| # TIER 3 — Arbitrary Value Parser | |
| # ===================================================================== | |
| _RE_ARBITRARY = re.compile(r'^([\w-]+)-\[(.+)\]$') | |
| _ARBITRARY_PREFIX_MAP = { | |
| "w": "width", "h": "height", | |
| "min-w": "min-width", "min-h": "min-height", | |
| "max-w": "max-width", "max-h": "max-height", | |
| "p": "padding", "px": "horizontal padding", "py": "vertical padding", | |
| "pt": "top padding", "pb": "bottom padding", | |
| "pl": "left padding", "pr": "right padding", | |
| "m": "margin", "mx": "horizontal margin", "my": "vertical margin", | |
| "mt": "top margin", "mb": "bottom margin", | |
| "ml": "left margin", "mr": "right margin", | |
| "gap": "gap", "gap-x": "horizontal gap", "gap-y": "vertical gap", | |
| "text": "text", "bg": "background", "border": "border", | |
| "rounded": "border radius", | |
| "top": "top offset", "bottom": "bottom offset", | |
| "left": "left offset", "right": "right offset", | |
| "inset": "inset", "z": "z-index", "opacity": "opacity", | |
| "basis": "flex basis", "tracking": "letter spacing", | |
| "leading": "line height", "indent": "text indent", | |
| "columns": "column count", | |
| "translate-x": "horizontal translate", "translate-y": "vertical translate", | |
| "rotate": "rotation", "skew-x": "horizontal skew", "skew-y": "vertical skew", | |
| "scale": "scale", "duration": "animation duration", "delay": "animation delay", | |
| "from": "gradient start", "via": "gradient middle", "to": "gradient end", | |
| "fill": "fill color", "stroke": "stroke color", | |
| "outline": "outline", "ring": "outline ring", | |
| "shadow": "shadow", "blur": "blur amount", | |
| "brightness": "brightness", "contrast": "contrast", | |
| "grayscale": "grayscale", "saturate": "saturation", | |
| "backdrop-blur": "backdrop blur", | |
| } | |
| def _describe_value(raw: str) -> str: | |
| """Describe the value inside arbitrary brackets.""" | |
| v = raw.strip() | |
| if v.startswith("var("): | |
| return f"CSS variable {v[4:].rstrip(')')}" | |
| if v.startswith("#"): | |
| return f"color {v}" | |
| if v.startswith("rgb"): | |
| return f"color {v}" | |
| if v.startswith("hsl"): | |
| return f"color {v}" | |
| if "calc(" in v: | |
| return f"calculated value {v}" | |
| if v.startswith("url("): | |
| return "URL resource" | |
| return v | |
| def _try_tier3(cls: str) -> str | None: | |
| """Attempt arbitrary value parsing. Returns explanation or None.""" | |
| m = _RE_ARBITRARY.match(cls) | |
| if not m: | |
| return None | |
| prefix = m.group(1) | |
| raw_value = m.group(2) | |
| described = _describe_value(raw_value) | |
| css_prop = _ARBITRARY_PREFIX_MAP.get(prefix) | |
| if css_prop: | |
| return f"{css_prop} set to {described}" | |
| readable = prefix.replace("-", " ") | |
| return f"{readable} set to {described}" | |
| # ===================================================================== | |
| # VARIANT HANDLING (Recursive) | |
| # ===================================================================== | |
| _VARIANT_MAP = { | |
| "sm": "on small screens (640px+)", | |
| "md": "on medium screens (768px+)", | |
| "lg": "on large screens (1024px+)", | |
| "xl": "on extra-large screens (1280px+)", | |
| "2xl": "on 2xl screens (1536px+)", | |
| "hover": "on hover", | |
| "focus": "on focus", | |
| "active": "when active", | |
| "disabled": "when disabled", | |
| "visited": "when visited", | |
| "dark": "in dark mode", | |
| "first": "for first child", | |
| "last": "for last child", | |
| "odd": "for odd children", | |
| "even": "for even children", | |
| "group-hover": "when parent group is hovered", | |
| "focus-within": "when a child has focus", | |
| "focus-visible": "on keyboard focus", | |
| "placeholder": "for placeholder text", | |
| "before": "for ::before pseudo-element", | |
| "after": "for ::after pseudo-element", | |
| "motion-safe": "when user allows motion", | |
| "motion-reduce": "when user prefers reduced motion", | |
| "print": "when printing", | |
| } | |
| def _resolve_class(cls: str) -> str: | |
| """Resolve a single class through all three tiers + recursive variants.""" | |
| if ":" in cls: | |
| colon_idx = cls.index(":") | |
| prefix = cls[:colon_idx] | |
| inner = cls[colon_idx + 1:] | |
| variant_desc = _VARIANT_MAP.get(prefix) | |
| if variant_desc: | |
| inner_desc = _resolve_class(inner) | |
| return f"{variant_desc}: {inner_desc}" | |
| exact = _EXACT.get(cls) | |
| if exact: | |
| return exact | |
| tier2 = _try_tier2(cls) | |
| if tier2: | |
| return tier2 | |
| tier3 = _try_tier3(cls) | |
| if tier3: | |
| return tier3 | |
| return f"{cls} \u2192 custom class \u2014 check your CSS file" | |
| # ===================================================================== | |
| # PUBLIC API | |
| # ===================================================================== | |
| def explain_classes(class_string: str, framework: str = "auto") -> str: | |
| """Explain a space-separated class attribute string in plain English. | |
| Returns pipe-separated explanations, one per class. | |
| framework: "tailwind", "bootstrap", or "auto" (detects from patterns). | |
| """ | |
| if not class_string or not class_string.strip(): | |
| return "" | |
| classes = class_string.split() | |
| explanations = [_resolve_class(cls) for cls in classes] | |
| return " | ".join(explanations) | |
| def detect_framework(soup) -> str: | |
| """Inspect a BeautifulSoup document for framework signals. | |
| Checks script/link tags for CDN references and class-name patterns. | |
| Returns "tailwind", "bootstrap", or "none". | |
| """ | |
| for tag in soup.find_all(["script", "link"]): | |
| src = tag.get("src", "") or tag.get("href", "") | |
| if not src: | |
| continue | |
| src_lower = src.lower() | |
| if "tailwind" in src_lower: | |
| return "tailwind" | |
| if "bootstrap" in src_lower: | |
| return "bootstrap" | |
| all_classes = [] | |
| for tag in soup.find_all(True): | |
| cls = tag.get("class") | |
| if cls: | |
| if isinstance(cls, list): | |
| all_classes.extend(cls) | |
| else: | |
| all_classes.extend(cls.split()) | |
| if not all_classes: | |
| return "none" | |
| tw_signals = 0 | |
| bs_signals = 0 | |
| tw_re = re.compile( | |
| r'^(sm|md|lg|xl|2xl):|^[pmwh][xytrbl]?-\d+$|^bg-[a-z]+-\d{3}$' | |
| r'|^text-(xs|sm|base|lg|xl|[2-9]xl)$' | |
| ) | |
| bs_exclusive = { | |
| "btn", "card", "modal", "navbar", "row", "d-flex", | |
| "d-none", "d-block", "container-fluid", "form-control", | |
| "alert", "badge", "dropdown", "accordion", "offcanvas", | |
| } | |
| for c in all_classes: | |
| if tw_re.search(c): | |
| tw_signals += 1 | |
| if c in bs_exclusive or c.startswith("btn-") or c.startswith("col-"): | |
| bs_signals += 1 | |
| if tw_signals > bs_signals: | |
| return "tailwind" | |
| if bs_signals > tw_signals: | |
| return "bootstrap" | |
| if tw_signals > 0: | |
| return "tailwind" | |
| return "none" | |