@taprootio/site-authoring 0.14.1 → 0.16.0
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.
- package/README.md +181 -16
- package/examples/riverbend-wellness/.taproot-site-media.json +23 -0
- package/examples/riverbend-wellness/pages/index.md +1 -17
- package/package.json +2 -2
- package/src/api.js +123 -1
- package/src/authors-contract.js +170 -0
- package/src/cli.js +355 -30
- package/src/constants.js +9 -0
- package/src/content/components.js +64 -6
- package/src/content/free-form-sections.js +21 -4
- package/src/content/link-warnings.js +109 -0
- package/src/content/markdown.js +161 -26
- package/src/content/tiptap-prosemirror.ts +66 -8
- package/src/content/validate-document.js +30 -6
- package/src/content/vocabulary.js +26 -0
- package/src/errors.js +22 -0
- package/src/espalier-shared.js +11 -0
- package/src/fixture-init.js +55 -3
- package/src/font-catalog.js +87 -0
- package/src/footer-contract.js +1 -1
- package/src/output.js +19 -1
- package/src/page-metadata.js +92 -0
- package/src/presentation-reference.js +21 -7
- package/src/problems.js +116 -0
- package/src/reference-help.js +157 -41
- package/src/session.js +13 -0
- package/src/staging-check.js +62 -0
- package/src/theme-proposals.js +393 -0
- package/src/theme-validation.js +31 -10
- package/src/typed-page-reference.js +97 -21
- package/src/typed-pages.js +196 -13
- package/src/verbs/approve.js +63 -14
- package/src/verbs/authors.js +167 -0
- package/src/verbs/deploy.js +5 -28
- package/src/verbs/index.js +22 -0
- package/src/verbs/media-upload.js +127 -30
- package/src/verbs/nav-push.js +7 -0
- package/src/verbs/pages-meta-set.js +303 -0
- package/src/verbs/pages-push.js +814 -224
- package/src/verbs/places.js +61 -0
- package/src/verbs/plan.js +595 -0
- package/src/verbs/pull.js +255 -20
- package/src/verbs/redirects-check.js +4 -10
- package/src/verbs/theme-push.js +68 -36
- package/src/verbs/validate.js +309 -217
- package/src/video-prepare.js +48 -12
- package/src/workspace-check.js +199 -0
- package/src/workspace-ledger.js +173 -0
- package/src/workspace.js +105 -10
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { SiteAuthoringError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Who a page can be credited to (TR01196).
|
|
5
|
+
*
|
|
6
|
+
* A page's author is one of two kinds of person, named by one string:
|
|
7
|
+
*
|
|
8
|
+
* - a site member who can create pages, by email address; or
|
|
9
|
+
* - a site author without a Taproot account, by handle. Handles are a
|
|
10
|
+
* lowercase slug the site fixes when the author is created, so a page keeps
|
|
11
|
+
* pointing at the same person if their display name changes.
|
|
12
|
+
*
|
|
13
|
+
* The two spellings cannot collide: a handle never contains '@'. The site
|
|
14
|
+
* folds case on both (an address is stored lowercase, a handle is lowercase),
|
|
15
|
+
* so this module does too, and compares only folded values.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const AUTHORS_FILE_NAME = "authors.json";
|
|
19
|
+
|
|
20
|
+
export const AUTHOR_HANDLE_MAXIMUM_LENGTH = 64;
|
|
21
|
+
export const AUTHOR_DISPLAY_NAME_MAXIMUM_LENGTH = 256;
|
|
22
|
+
export const AUTHOR_EMAIL_MAXIMUM_LENGTH = 320;
|
|
23
|
+
|
|
24
|
+
const HANDLE = /^[a-z0-9]+(-[a-z0-9]+)*$/u;
|
|
25
|
+
// Deliberately loose: the site canonicalizes and decides. This only tells an
|
|
26
|
+
// address from a handle and refuses text that cannot be one.
|
|
27
|
+
const EMAIL = /^[^\s@<>()]+@[^\s@<>()]+\.[^\s@<>()]+$/u;
|
|
28
|
+
|
|
29
|
+
export const AUTHOR_KIND_HANDLE = "handle";
|
|
30
|
+
export const AUTHOR_KIND_EMAIL = "email";
|
|
31
|
+
|
|
32
|
+
/** The folded handle, or undefined when the text is not a handle. */
|
|
33
|
+
export function normalizeAuthorHandle(value) {
|
|
34
|
+
if (typeof value !== "string") return undefined;
|
|
35
|
+
const handle = value.trim().toLowerCase();
|
|
36
|
+
return handle.length > 0 && handle.length <= AUTHOR_HANDLE_MAXIMUM_LENGTH && HANDLE.test(handle)
|
|
37
|
+
? handle
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The folded address, or undefined when the text is not an email address. */
|
|
42
|
+
export function normalizeAuthorEmail(value) {
|
|
43
|
+
if (typeof value !== "string") return undefined;
|
|
44
|
+
const email = value.trim().toLowerCase();
|
|
45
|
+
return email.length <= AUTHOR_EMAIL_MAXIMUM_LENGTH && EMAIL.test(email) ? email : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What an `author` reference names: `{ kind, value }` with the folded value, or
|
|
50
|
+
* undefined for text that is neither a handle nor an email address.
|
|
51
|
+
*/
|
|
52
|
+
export function parseAuthorReference(value) {
|
|
53
|
+
if (typeof value !== "string") return undefined;
|
|
54
|
+
if (value.includes("@")) {
|
|
55
|
+
const email = normalizeAuthorEmail(value);
|
|
56
|
+
return email === undefined ? undefined : { kind: AUTHOR_KIND_EMAIL, value: email };
|
|
57
|
+
}
|
|
58
|
+
const handle = normalizeAuthorHandle(value);
|
|
59
|
+
return handle === undefined ? undefined : { kind: AUTHOR_KIND_HANDLE, value: handle };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The folded reference, or a refusal that names where it was written. */
|
|
63
|
+
export function requireAuthorReference(value, field, code = "pages.author_invalid") {
|
|
64
|
+
const reference = parseAuthorReference(value);
|
|
65
|
+
if (reference === undefined) {
|
|
66
|
+
throw new SiteAuthoringError(
|
|
67
|
+
code,
|
|
68
|
+
`'${typeof value === "string" ? value.slice(0, 80) : ""}' is not an author reference. Name a site author by `
|
|
69
|
+
+ "handle (lowercase letters, digits and hyphens, such as jane-doe) or a site member by email address.",
|
|
70
|
+
{ field },
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return reference.value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function boundedText(value, maximum) {
|
|
77
|
+
return typeof value === "string" && value.length <= maximum ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Reads the site's author listing, refusing a response that does not have the
|
|
82
|
+
* shape this CLI relies on. A member's address is only present for a caller
|
|
83
|
+
* allowed to see it; the CLI's credential is, so an absent one is dropped.
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeAuthorListing(value) {
|
|
86
|
+
const listing = value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
87
|
+
const authors = [];
|
|
88
|
+
for (const raw of Array.isArray(listing.authors) ? listing.authors : []) {
|
|
89
|
+
const handle = normalizeAuthorHandle(raw?.handle);
|
|
90
|
+
if (handle === undefined) continue;
|
|
91
|
+
authors.push({
|
|
92
|
+
handle,
|
|
93
|
+
displayName: boundedText(raw.displayName, AUTHOR_DISPLAY_NAME_MAXIMUM_LENGTH) ?? "",
|
|
94
|
+
hasEmail: raw.hasEmail === true || (typeof raw.email === "string" && raw.email !== ""),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const members = [];
|
|
98
|
+
for (const raw of Array.isArray(listing.members) ? listing.members : []) {
|
|
99
|
+
const email = normalizeAuthorEmail(raw?.email);
|
|
100
|
+
if (email === undefined) continue;
|
|
101
|
+
members.push({
|
|
102
|
+
email,
|
|
103
|
+
displayName: boundedText(raw.displayName, AUTHOR_DISPLAY_NAME_MAXIMUM_LENGTH) ?? "",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return { authors, members };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The workspace file `pull` and `authors list` write. Site authors carry no
|
|
111
|
+
* address: it is private to the site and nothing local needs it. Members carry
|
|
112
|
+
* theirs, because that is how a page names them.
|
|
113
|
+
*/
|
|
114
|
+
export function projectAuthorsForWorkspace(siteId, listing) {
|
|
115
|
+
return {
|
|
116
|
+
siteId,
|
|
117
|
+
authors: listing.authors.map(({ handle, displayName }) => ({ handle, displayName })),
|
|
118
|
+
members: listing.members.map(({ email, displayName }) => ({ email, displayName })),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reads `authors.json` back. Anything unreadable or bound to another site is
|
|
124
|
+
* treated as absent: it can only ever make a reference unprovable offline,
|
|
125
|
+
* never turn a valid one into a refusal.
|
|
126
|
+
*/
|
|
127
|
+
export function readAuthorsDocument(value, siteId) {
|
|
128
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value.siteId !== siteId) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const handles = new Set();
|
|
132
|
+
for (const author of Array.isArray(value.authors) ? value.authors : []) {
|
|
133
|
+
const handle = normalizeAuthorHandle(author?.handle);
|
|
134
|
+
if (handle !== undefined) handles.add(handle);
|
|
135
|
+
}
|
|
136
|
+
const emails = new Set();
|
|
137
|
+
for (const member of Array.isArray(value.members) ? value.members : []) {
|
|
138
|
+
const email = normalizeAuthorEmail(member?.email);
|
|
139
|
+
if (email !== undefined) emails.add(email);
|
|
140
|
+
}
|
|
141
|
+
return { handles, emails };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether a folded reference names someone the recorded listing knows, or
|
|
146
|
+
* `undefined` when there is no listing to ask.
|
|
147
|
+
*/
|
|
148
|
+
export function authorKnown(authors, reference) {
|
|
149
|
+
if (authors === undefined) return undefined;
|
|
150
|
+
const parsed = parseAuthorReference(reference);
|
|
151
|
+
if (parsed === undefined) return false;
|
|
152
|
+
return parsed.kind === AUTHOR_KIND_EMAIL ? authors.emails.has(parsed.value) : authors.handles.has(parsed.value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The refusal text for a source that names a different author than the site
|
|
157
|
+
* holds. The way out depends on where the author is written: a `.pm.json`
|
|
158
|
+
* page keeps it in the workspace manifest, edited with `pages meta set`; a
|
|
159
|
+
* Markdown page keeps it in its front matter (TR01196).
|
|
160
|
+
*/
|
|
161
|
+
export function authorConflictMessage({ shown, file, held, wanted, markdown }) {
|
|
162
|
+
const lead = `Page '${shown}' already has the author '${held}'`
|
|
163
|
+
+ (wanted === undefined ? "" : `, and '${file}' names '${wanted}'`)
|
|
164
|
+
+ ". An author is never replaced from the CLI";
|
|
165
|
+
return markdown
|
|
166
|
+
? `${lead}: set 'author: ${held}' in the front matter, or remove the line, to match the site. To credit someone `
|
|
167
|
+
+ "else, change it in the app."
|
|
168
|
+
: `${lead}: run 'taproot-site pages meta set ${shown} --author ${held}' to match the site. To credit someone `
|
|
169
|
+
+ "else, change it in the app.";
|
|
170
|
+
}
|