//
->
// ->
// -> unchanged: utilities keep class
//
// The rules, in full:
//
// - A kind becomes a bare attribute.
// - A BEM part (name__part) becomes a flat attribute (name-part). The
// value slot never names a part, so the flattening is what keeps
// `rst-btn="primary"` meaning "variant" on every attribute.
// - A BEM modifier (name--variant) becomes a token in its kind's
// value, matched with ~= — the same space-separated matching a class
// list gets, so two variants compose with no new mechanism.
// - data-tone becomes rst-tone. Every other data-* attribute carries
// runtime state (data-busy, data-poll, data-theme) and is untouched.
// - Seven utilities keep class, because class is what cross-cutting
// styling is for. An element with a kind, a variant and a utility
// ends up with an attribute and a class, and that is correct.
//
// # Why this is a program
//
// The framework had ~700 call sites; an app has its own. Hand edits at
// that scale contain hand errors, and the same translation has to run
// over repositories this one cannot see. So the transformation is a
// tool, `rastrillo markup`, and this package is what the tool, the
// stage-1 tokens.css gate and the browser drive all call — one grammar,
// not three implementations that agree until they do not.
//
// Rewriting is idempotent: the output carries no class= attribute the
// input side of the grammar can match, so a second pass changes
// nothing.
package markup
import (
"fmt"
"regexp"
"strings"
)
// Utilities are the seven classes that keep the class spelling, with
// the reason. They are not kinds — they are cross-cutting styling,
// which is what class is for — so they stay class past stage 3 rather
// than being migrated and un-migrated.
var Utilities = map[string]string{
"rst-sr-only": "utility: visually-hidden text",
"rst-mono": "utility: monospaced value",
"rst-m-hide": "utility: hidden on a narrow screen",
"rst-grow": "utility: this flex child takes the slack",
"rst-nm": "utility: the name cell's type",
"rst-danger": "utility: a destructive item's colour",
"rst-cell-mut": "utility: a muted, truncating cell",
}
// Renamed are the classes the flip renames outright rather than
// translating, because translating them would collide.
//
// rst-form__foot and rst-form-foot both existed and both were live:
// the plain action row the form-foot partial emits, and the sticky save
// bar an app writes by hand. BEM's __ flattens to a hyphen, so the two
// wanted one attribute. The partial's row takes the name the partial is
// called; the save bar, which no partial emits, becomes rst-form-bar.
//
// Order does not matter here: a class token is looked up once, so
// rst-form-foot -> rst-form-bar and rst-form__foot -> rst-form-foot do
// not chain.
var Renamed = map[string]string{
"rst-form__foot": "rst-form-foot",
"rst-form-foot": "rst-form-bar",
"rst-form-foot__note": "rst-form-bar__note",
}
// Dropped are classes that are deleted rather than translated, because
// they style nothing: an attribute spelling of them would be a name the
// stylesheet has never heard of, sitting in markup an LLM copies.
var Dropped = map[string]string{
"rst-dropdown__summary": "no rule: the summary is styled structurally, by [rst-dropdown] > summary",
"rst-btn__spin": "no rule: the busy spinner is styled structurally, by [rst-btn] > [rst-spin]",
"rst-combo__input": "no rule: select.js wrote it beside rst-input and nothing ever styled it",
}
// Attribute translates one rst- class name into the attribute that
// styles the same thing. ok is false for a class that keeps its
// spelling. It is the pure grammar — no renames, no deletions — so it
// is also what translates a tokens.css selector, where the renames have
// already happened in the file itself.
func Attribute(class string) (name, variant string, ok bool) {
if _, exempt := Utilities[class]; exempt {
return "", "", false
}
body := strings.TrimPrefix(class, "rst-")
if k := strings.Index(body, "--"); k >= 0 {
variant = body[k+2:]
body = body[:k]
}
return "rst-" + strings.ReplaceAll(body, "__", "-"), variant, true
}
// classFor is Attribute plus the two things only a markup rewrite does:
// the deletions, and — for a migration rather than a respelling — the
// renames. drop is true for a class that leaves the markup entirely.
//
// The renames belong to the migration alone. They translate markup
// written before the flip, where rst-form-foot meant the sticky save
// bar; markup already written in today's class vocabulary means the
// action row by that name, and running the rename over it would move it
// to rst-form-bar and change what renders. That distinction is why
// Respell exists beside Rewrite.
func classFor(class string, migrate bool) (name, variant string, ok, drop bool) {
if _, gone := Dropped[class]; gone {
return "", "", false, true
}
if to, renamed := Renamed[class]; migrate && renamed {
class = to
}
name, variant, ok = Attribute(class)
return name, variant, ok, false
}
var (
classInSelector = regexp.MustCompile(`\.(rst-[A-Za-z0-9_-]+)`)
toneInSelector = regexp.MustCompile(`\[data-tone="([a-z]+)"\]`)
// A data-tone that is a real attribute rather than a selector. A
// selector's follows a [; an attribute's follows whitespace, or the
// opening quote of the Go literal a test writes it in.
toneInMarkup = regexp.MustCompile("([\\s`'\"])data-tone=(\\\\?\")")
)
// Selector is the attribute twin of one tokens.css selector: every
// translatable rst- class swapped for its attribute, and data-tone for
// rst-tone. Everything else — type selectors, combinators,
// pseudo-classes, .icon, an app's own class, and the data-* attributes
// that carry runtime state — is left exactly as it is. A selector with
// nothing to translate comes back unchanged, which is how the stage-1
// gate tells "needs a twin" from "is one".
func Selector(selector string) string {
out := classInSelector.ReplaceAllStringFunc(selector, func(m string) string {
name, variant, ok := Attribute(m[1:])
if !ok {
return m
}
if variant == "" {
return "[" + name + "]"
}
return fmt.Sprintf("[%s~=%q]", name, variant)
})
return toneInSelector.ReplaceAllString(out, `[rst-tone~="$1"]`)
}
|