webcraft-ai-backend / utils /class_explainer.py
3ssem0
feat(ai): update code commenter, title generator, and add class explainer utils
2739df8
Raw
History Blame Contribute Delete
41.2 kB
"""
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"