// fixtures/fonts.mjs — the font vocabulary, in one place. // // Three faces, one of which is the default. That last word is the whole // reason this file exists, because the default is not a property of any // face: it is a decision, held in four places that cannot see each // other, and until it moved nothing had to state it. // // - views/show.html's `[docs-text]` rule, which is what an unmarked // run is set in on screen; // - internal/export/pdf.go's styleFor, which is what an unmarked run // is set in on paper; // - internal/fonts' `Default`, which names the face those two must // agree on; // - and this module, which is what the toolbar SAYS an unmarked run // is set in. // // A toolbar disagreeing with the other three is the quiet failure. It // does not throw and it does not lose anything: the font control simply // reads "Serif" over text that is drawn in sans, and a person who wants // serif selects it, watches the control already say Serif, and gets // nothing. internal/docs's TestTheToolbarDefaultFontIsTheDefaultFace is // what holds this file's DEFAULT_FONT to internal/fonts' Default, since // Go and Node cannot import each other. // // The rule the whole vocabulary turns on: THE DEFAULT CARRIES NO MARK. // Choosing it clears the group rather than setting anything, so a // document only ever contains a font mark where somebody asked for a // face other than the one they would have got anyway. That is what // makes a document written before any of this existed render as today's // default, and it is why moving the default is a one-line change here // with real consequences rather than a cosmetic relabelling. // The faces a person can choose, in the order the toolbar shows them — // default first, because a shows. // // Runs at module load, like toolbar.mjs's alignmentsAreCovered and for // the reason that one's comment gives at length: a guard that only runs // when a browser mounts the editor is dead code in the gate, so a // vocabulary that had drifted would pass CI and throw for every user. // fonts.test.mjs imports this module, which is what makes this call // execute on every gate run. function fontsAreCovered() { const marked = FONT_MARKS.map((m) => m.slice('font_'.length)) if ([...marked].sort().join() !== [...FONTS].sort().join()) { throw new Error(`fonts: marks [${marked}] do not match the faces [${FONTS}]`) } if (!FONTS.includes(DEFAULT_FONT)) { throw new Error(`fonts: the default ${JSON.stringify(DEFAULT_FONT)} is not one of [${FONTS}]`) } if (FONTS[0] !== DEFAULT_FONT) { throw new Error( `fonts: the toolbar lists ${JSON.stringify(FONTS[0])} first but the default is ` + `${JSON.stringify(DEFAULT_FONT)} — with no JavaScript the control would name a face the text is not in`, ) } // There is deliberately no fourth check that fontMark(DEFAULT_FONT) // is null. One was written here and deleted: fontMark returns null // exactly when its argument is in FONTS and equals DEFAULT_FONT, and // the check above has already thrown if DEFAULT_FONT is not in FONTS // — so it compared a value to its own derivation and could not throw // for ANY value of DEFAULT_FONT. Verified rather than reasoned: // running fontMark over every face and over three non-faces makes the // condition false in all six cases. // // This is the eighth assertion found in this repo unable to fail // against the thing it names, and all eight have had one shape — a // value compared to itself, or to something derived from itself. "The // default is spelled as the absence of a mark" is a property of // fontMark's own definition, so the place to state it is that // function, and the place to TEST it is fonts.test.mjs, where the // expectation is a literal. } fontsAreCovered()