@casomoltd/tooling 0.33.2 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * A site's colophon as Buttondown `socials` links, for a footer that
3
+ * says what the site's footer says. Buttondown draws `socials` only as
4
+ * links, and caps each label (docs/buttondown-platform.md, The API), so
5
+ * a colophon line arrives as several links that a skin sets as one line
6
+ * of text.
7
+ */
8
+
9
+ /** Buttondown's limit on a `socials` label, and the only `socials` type
10
+ * that carries a label of our own. */
11
+ export const SOCIALS_LABEL_MAX = 32;
12
+ const GENERIC = "generic";
13
+
14
+ /** The query a skin keys on, and its parts: `<line>-<part>`. */
15
+ const MARK = "colophon";
16
+ const START = "start";
17
+ const RUN = "run";
18
+ const NUMBER = "num";
19
+
20
+ /**
21
+ * The links for each colophon line: its text in runs that fit a label,
22
+ * then, for a line with a register, the register's number with the full
23
+ * stop (one label, so the stop can't wrap away from it); a line with a
24
+ * source gets the full stop on its text instead. A line is `{text,
25
+ * register: {number, url}}` or `{text, source}`, its text written
26
+ * without the closing full stop; every run links to the register, or to
27
+ * the source where a line without one is explained or published.
28
+ *
29
+ * Each URL carries a `colophon` query, `<line>-<part>`: `start` begins a
30
+ * line, `num` is the number, and `run<n>` the rest of the line's text. A
31
+ * query rather than a fragment, so a link to a section keeps its anchor.
32
+ */
33
+ export function colophonLinks(colophon) {
34
+ return colophon.flatMap((entry, line) => {
35
+ const {text, register, source} = entry;
36
+ if (text.endsWith(".")) {
37
+ throw new Error(`colophon line "${text}": leave off the full stop`);
38
+ }
39
+ if (register && !(register.url && register.number)) {
40
+ throw new Error(`colophon line "${text}": register needs url and number`);
41
+ }
42
+ const url = register ? register.url : source;
43
+ if (!url) throw new Error(`colophon line "${text}" links nowhere`);
44
+ const at = (part) => {
45
+ const marked = new URL(url);
46
+ if (!["http:", "https:"].includes(marked.protocol)) {
47
+ throw new Error(`colophon line "${text}": ${url} isn't http(s)`);
48
+ }
49
+ marked.searchParams.set(MARK, `${line}-${part}`);
50
+ return marked.href;
51
+ };
52
+ const runs = labelRuns(register ? text : `${text}.`);
53
+ return [
54
+ ...runs.map((label, i) => ({
55
+ type: GENERIC, url: at(i === 0 ? START : `${RUN}${i}`), label,
56
+ })),
57
+ ...(register
58
+ ? [{type: GENERIC, url: at(NUMBER), label: `${register.number}.`}]
59
+ : []),
60
+ ];
61
+ });
62
+ }
63
+
64
+ /** `text` split at spaces into runs of at most SOCIALS_LABEL_MAX
65
+ * characters; throws on a word longer than that, which no run holds. */
66
+ export function labelRuns(text) {
67
+ const out = [];
68
+ for (const word of text.split(" ")) {
69
+ const last = out.at(-1);
70
+ if (last && `${last} ${word}`.length <= SOCIALS_LABEL_MAX) {
71
+ out[out.length - 1] = `${last} ${word}`;
72
+ } else {
73
+ if (word.length > SOCIALS_LABEL_MAX) {
74
+ throw new Error(`no socials label fits "${word}"`);
75
+ }
76
+ out.push(word);
77
+ }
78
+ }
79
+ return out;
80
+ }
@@ -47,6 +47,8 @@ export const END = "/* END TOKENS */";
47
47
  export const EMAIL_CHROME_HIDES = {
48
48
  ".social-links": "the web footer's links, duplicated in an email",
49
49
  ".newsletter-masthead": "replaced by the repo's own header band",
50
+ ".transactional-email-header":
51
+ "the icon, subject and name above a transactional email's own bands",
50
52
  ".newsletter-icon": "the icon slot above the repo's own mark",
51
53
  ".email-preamble": "the forwarding prompt between title and prose",
52
54
  ".colophon": "the sender-and-date line under the hidden masthead",
@@ -123,3 +123,29 @@ export function composeWebSkin(site: string): string;
123
123
 
124
124
  /** The shared email rules, in the site's theme, with literal values. */
125
125
  export function emailBaseCss(theme: EmailTheme): string;
126
+
127
+ /** The register that proves a colophon line, by its number. */
128
+ export interface Register {
129
+ readonly number: string;
130
+ readonly url: string;
131
+ }
132
+
133
+ /** One colophon line, its text without the closing full stop: then the
134
+ * register that proves it, or the http(s) source where a line without
135
+ * one is explained or published. */
136
+ export type ColophonLine =
137
+ | {readonly text: string; readonly register: Register; readonly source?: never}
138
+ | {readonly text: string; readonly source: string; readonly register?: never};
139
+
140
+ /** A labelled Buttondown `socials` entry. */
141
+ export interface ColophonLink {
142
+ type: "generic";
143
+ url: string;
144
+ label: string;
145
+ }
146
+
147
+ /** A colophon as `socials` links, each URL marked with a `colophon`
148
+ * query (`<line>-start|run<n>|num`) for a skin to key on. */
149
+ export function colophonLinks(
150
+ colophon: readonly ColophonLine[],
151
+ ): ColophonLink[];
@@ -5,5 +5,6 @@
5
5
  export {
6
6
  buildPayload, EMAIL_CHROME_HIDES, loadSettings, tokenReader,
7
7
  } from "./engine.mjs";
8
+ export {colophonLinks} from "./colophon.mjs";
8
9
  export {createEmailBlocks, TABLE} from "./email-blocks.mjs";
9
10
  export {composeWebSkin, emailBaseCss} from "./skins.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casomoltd/tooling",
3
- "version": "0.33.2",
3
+ "version": "0.34.1",
4
4
  "description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets and dev CLIs (git hooks, readability, screenshots, design specs)",
5
5
  "license": "MIT",
6
6
  "repository": {