//go:build fiestaseed // The Fiesta prose exporter for internal/designsystem. hack/seed/run-prose.sh // copies this file into a scratch rastrillo worktree at the seed commit // and runs TestFiestaProse there, so it reads the gallery's English // where it is written: the family tables, the idiom blurbs, the shell // views, the format and screen docs, and the two idiom rules. It // reads, under FIESTA_SEED_OUT, the committed examples/examples.json // and hack/seed/sources.json, and writes two files there: // // - examples/prose.json, every explanation the gallery shows, keyed // by family, example and state, with a screen's warning callout as // a title and body of its own beside the state's note; // - hack/seed/prose-review.json, the strings that could not carry // verbatim, each with the rastrillo text, the reason, and the // approved replacement once there is one. // // A string joins the review queue when it mentions something Fiesta // does not have (a Go template, a partial, a server, a cookie, // rastrillo itself) or when, read against Fiesta's HTML, it is not // true. The mechanical part is proseReason, over proseWords, // proseFields and proseScripts; the rest is a judgement, written down // in proseUntrue with its reason. A queued // string's slot in prose.json is empty until approvedProse carries // its replacement, so a reseed reproduces the approved text and never // the rastrillo one. package designsystem import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "sort" "strings" "testing" ) // The manifest's shape and sources.json's, mirrored from Fiesta. The // tags are the contract; this side cannot import the other. type proseManifest struct { Examples []proseEntry `json:"examples"` } type proseEntry struct { Name string `json:"name"` Family string `json:"family"` Kind string `json:"kind"` States []proseState `json:"states"` } type proseState struct { ID string `json:"id"` } type proseSource struct { Kind string `json:"kind"` Source string `json:"source"` State int `json:"state"` } // prose.json's shape, mirrored from Fiesta's gallery package. type proseFile struct { Families map[string]proseFamily `json:"families"` Examples map[string]proseExample `json:"examples"` Pages prosePages `json:"pages"` } type proseFamily struct { Title string `json:"title"` Blurb string `json:"blurb"` } type proseExample struct { Blurb string `json:"blurb"` States map[string]string `json:"states,omitempty"` Warnings map[string]proseWarning `json:"warnings,omitempty"` } // proseWarning is the callout after one state's note: a screen's // WarningTitle and WarningBody, each a slot of its own. type proseWarning struct { Title string `json:"title"` Body string `json:"body"` } type prosePages struct { Idioms []proseRule `json:"idioms"` } type proseRule struct { Idiom string `json:"idiom"` Title string `json:"title"` Body string `json:"body"` } // proseReview is one line of hack/seed/prose-review.json: the slot, // what rastrillo says there, why that cannot carry, and what Fiesta // says instead once copy review has approved it. type proseReview struct { ID string `json:"id"` Seed string `json:"seed"` Reason string `json:"reason"` Text string `json:"text"` } // proseWords are the words that put a string in the review queue: // each names something Fiesta's HTML does not have. var proseWords = []struct { what string re *regexp.Regexp }{ {"Go", regexp.MustCompile(`\bGo\b`)}, {"a template", regexp.MustCompile(`(?i)\btemplates?\b`)}, {"{{", regexp.MustCompile(`\{\{`)}, {"ui.", regexp.MustCompile(`\bui\.`)}, {"rastrillo", regexp.MustCompile(`(?i)rastrillo`)}, {"a partial", regexp.MustCompile(`(?i)\bpartials?\b`)}, {"/_theme", regexp.MustCompile(`/_theme`)}, {"a cookie", regexp.MustCompile(`(?i)\bcookies?\b`)}, {"a server", regexp.MustCompile(`(?i)\bservers?\b`)}, {".go", regexp.MustCompile(`\.go\b`)}, } // proseFields are the capitalised field names a note uses as a // template argument. Only the names no English sentence here starts // with: Label, Name, Title, Select, Page and Path are judged by eye // instead, in proseUntrue. var proseFields = []string{ "Lead", "Exact", "Primary", "Hidden", "ClearHref", "Placeholder", "Fit", "Desc", "SelectName", "CancelLabel", "Alert", "Tone", "Options", "Plain", "RevokePath", "RevokeAgentPath", "RevokeAllPath", "Created", "Currency", "Render", "PageData", } // proseScripts are the script files a string may name: the ones // Fiesta ships under that name. Any other .js in a string is a // script Fiesta does not have. var proseScripts = map[string]bool{ "anchor.js": true, "busy.js": true, "calendar.js": true, "copy.js": true, "datetime.js": true, "fiesta.js": true, "light.js": true, "select.js": true, "shell.js": true, } var proseScript = regexp.MustCompile(`\b[a-z][a-z0-9-]*\.js\b`) // proseUntrue is the judgement: strings that pass the word list and // are still not true of Fiesta's HTML, by slot, with the reason. var proseUntrue = map[string]string{ "examples.pagination.states.first-page-default-accessible-name": "Label is the template's argument, and the catalog is rastrillo's", "examples.field-text.blurb": "Name is the template's argument; in the HTML the ids are written out", "examples.field-daterange.blurb": "the derived ids are the template's; in the HTML they are written out", "examples.notice.blurb": "describes the template's argument, which the HTML does not have", "examples.form-error.blurb": "describes the template's argument, which the HTML does not have", "examples.field-select.blurb": "the ten-option rule is the template's; in the HTML the data-rst-select attribute is what arms select.js", "examples.job-status.blurb": "Fiesta ships no script that polls, and nothing here emits the attribute", "examples.job-status.states.running": "Fiesta ships no script that polls, so this state asks the network for nothing", "examples.layout-column.blurb": "Fiesta scaffolds nothing", "examples.format-money.blurb": "money is a template function", "examples.field-date.states.enhanced-input-type-tomorrow-or-in-3-weeks": "the page has no catalog; the words are on the attribute, and i18n/.json has the other languages", "examples.field-datetime.states.enhanced-input-type-tomorrow-or-in-3-weeks": "the page has no catalog; the words are on the attribute, and i18n/.json has the other languages", "examples.error-page.states.anything-else-the-generic-pair": "the framework catalog is rastrillo's; Fiesta's example is six documents", } // proseAlways are the slots that are queued whatever rastrillo says // there, with the reason. The standalone scheme menu is the one state // Fiesta wrote itself: its page remembers the choice, and rastrillo's // note describes a server. The sign-in example has no sentence about // the set of screens, because screenDocs() explains each one and not // the whole. Two of its screens' notes describe what the app behind // the screen does, which Fiesta's static example cannot: one has a // hard-coded address and forms that post, the other a link to # and // nothing that moves on. var proseAlways = map[string]string{ "examples.scheme-menu.states.standalone": "rastrillo's note describes a server; this state's page remembers the choice itself", "examples.signin.blurb": "no rastrillo source: screenDocs() explains each screen and not the set, so this one sentence is new", "examples.signin.states.coming-back-with-keymail": "claims behaviour the static example lacks: remembering and checking the address", "examples.signin.states.on-the-way-to-keymail": "claims behaviour the static example lacks: moving on by itself", } // approvedProse is the review's answer, by slot. Nothing joins this // map without copy review; a reseed writes these and never the // rastrillo text they replace. var approvedProse = map[string]string{ "examples.actor.states.an-agent-acting-for-a-person": "The agent's name, then the person it acted for. When the app itself did something, show nothing.", "examples.api-tokens.blurb": "The API token page: the one-time reveal, the create form, the list and the revoke buttons.", "examples.api-tokens.states.four-tokens-two-of-them-live": "The revoke forms post to /tokens/revoke, /tokens/revoke-agent and /tokens/revoke-all, under the page's own path. Point them wherever your app handles them.", "examples.api-tokens.states.just-created-the-secret-shown-once": "Show this in the response to the create form itself, and do not redirect it: the secret is in that response and nowhere else.", "examples.callout.states.negative-announced-as-an-alert": "role=\"alert\" makes a screen reader announce it as soon as it appears. Keep it for messages that cannot wait.", "examples.choice-field.blurb": "Option cards: the preferred radio or checkbox group. The title stays short; the description explains.", "examples.confirm-form.states.ordinary-default-cancel-wording": "The cancel link says Cancel, which is the usual wording. Change it when the form's own words are clearer.", "examples.error-page.states.anything-else-the-generic-pair": "Any status that has no wording of its own gets this generic pair: a title that says something is wrong and a body that says to try again.", "examples.field-date.blurb": "A native date input, plus the natural-language combobox datetime.js adds when the input carries data-rst-date. Type \"tomorrow\" into it.", "examples.field-date.states.enhanced-input-type-tomorrow-or-in-3-weeks": "The words it parses are on data-rst-date-words, here in the page's language. i18n/.json has them in twelve languages, so the field can parse whichever language the page is in.", "examples.field-daterange.blurb": "A start and an end as one labelled fieldset. Give every input its own name and id.", "examples.field-daterange.states.date-and-time-seeded": "Commit the start with the end still empty and datetime.js fills the end in an hour later. Nothing in the markup does that; the script does.", "examples.field-datetime.states.enhanced-input-type-tomorrow-or-in-3-weeks": "The words it parses are on data-rst-date-words, here in the page's language. i18n/.json has them in twelve languages, so the field can parse whichever language the page is in.", "examples.field-money.blurb": "A money amount in one currency. The currency code shows beside the label and is sent with the form, so a currency that changed while someone was typing is caught on save.", "examples.field-select.blurb": "The field envelope around a native select. Add data-rst-select and select.js turns it into a filterable combobox; the native select is still what the form submits. Worth it from about ten options.", "examples.field-select.states.hand-written-with-optgroups": "Grouped choices are optgroups you write yourself. select.js keeps the groups rather than dropping them; a select can also refuse the enhancement outright with data-rst-select=\"false\".", "examples.field-select.states.twelve-options-the-filterable-combobox": "Add data-rst-select from about ten options. Below that, leave it off: search over a handful of items is furniture, not help.", "examples.field-text.blurb": "One labelled text input. The required marker's star is aria-hidden; the input's required attribute is the programmatic signal.", "examples.field-text.states.the-headline-field": "Give the main field of a form, a title or a name, rst-input=\"primary\" for a more prominent input.", "examples.field-url.blurb": "A web address field that accepts example.com with or without https://. Whatever reads the form has to add the scheme and check the address.", "examples.form-error.blurb": "A form's summary error line, role=\"alert\". Leave it out when there is no error.", "examples.form-foot.states.idle-the-button-before-anything-happens": "A button that CHANGES something gets a loading state; a button that only reveals something, e.g. a disclosure, a dropdown, a tab, does not. busy.js applies that rule to every submit button in every form, with nothing to opt into.", "examples.form-foot.states.working-what-busy-js-writes-on-the-way-out": "Only the button that was clicked: every other submit button in the form keeps its name and its value, and the form itself is guarded against a second submit. data-busy=\"false\" opts out, on the form or on one button; data-busy-label replaces the text. With scripts off none of this happens and the form submits exactly as it always did, so a second submit is still your app's job to catch.", "examples.form-layout.blurb": "The attributes that give a form its rhythm and its save bar. They wrap a run of fields you compose yourself.", "examples.format-dates.blurb": "Put a date in a