""" 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"