@dogsbay/format-astro 0.2.0-beta.15 → 0.2.0-beta.151

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.
Files changed (67) hide show
  1. package/dist/api-markdown.d.ts +7 -0
  2. package/dist/api-markdown.js +49 -0
  3. package/dist/base-path.d.ts +93 -8
  4. package/dist/base-path.js +122 -9
  5. package/dist/blog.d.ts +133 -0
  6. package/dist/blog.js +325 -0
  7. package/dist/browser.d.ts +13 -0
  8. package/dist/browser.js +13 -0
  9. package/dist/canonical.d.ts +35 -0
  10. package/dist/canonical.js +110 -0
  11. package/dist/capabilities.d.ts +28 -0
  12. package/dist/capabilities.js +94 -0
  13. package/dist/cli.d.ts +0 -1
  14. package/dist/cli.js +3 -1
  15. package/dist/diff-decoration.d.ts +35 -0
  16. package/dist/diff-decoration.js +101 -0
  17. package/dist/granularity.d.ts +82 -0
  18. package/dist/granularity.js +246 -0
  19. package/dist/head-metadata.d.ts +25 -0
  20. package/dist/head-metadata.js +55 -0
  21. package/dist/index.d.ts +26 -6
  22. package/dist/index.js +34 -5
  23. package/dist/lead.d.ts +1 -39
  24. package/dist/lead.js +3 -38
  25. package/dist/llms-txt.d.ts +41 -11
  26. package/dist/llms-txt.js +82 -274
  27. package/dist/plugins.d.ts +0 -1
  28. package/dist/plugins.js +4 -3
  29. package/dist/project.d.ts +461 -13
  30. package/dist/project.js +2343 -649
  31. package/dist/redirects.d.ts +117 -0
  32. package/dist/redirects.js +223 -0
  33. package/dist/serialize.d.ts +14 -1
  34. package/dist/serialize.js +857 -255
  35. package/dist/site-chrome.d.ts +62 -0
  36. package/dist/site-chrome.js +56 -0
  37. package/dist/sitemap.d.ts +62 -4
  38. package/dist/sitemap.js +63 -56
  39. package/dist/taxonomy.d.ts +48 -2
  40. package/dist/taxonomy.js +50 -12
  41. package/dist/theme-css.d.ts +33 -0
  42. package/dist/theme-css.js +683 -0
  43. package/dist/tone-classes.d.ts +52 -0
  44. package/dist/tone-classes.js +84 -0
  45. package/dist/write-if-changed.d.ts +20 -0
  46. package/dist/write-if-changed.js +57 -0
  47. package/package.json +17 -7
  48. package/dist/base-path.d.ts.map +0 -1
  49. package/dist/base-path.js.map +0 -1
  50. package/dist/cli.d.ts.map +0 -1
  51. package/dist/cli.js.map +0 -1
  52. package/dist/index.d.ts.map +0 -1
  53. package/dist/index.js.map +0 -1
  54. package/dist/lead.d.ts.map +0 -1
  55. package/dist/lead.js.map +0 -1
  56. package/dist/llms-txt.d.ts.map +0 -1
  57. package/dist/llms-txt.js.map +0 -1
  58. package/dist/plugins.d.ts.map +0 -1
  59. package/dist/plugins.js.map +0 -1
  60. package/dist/project.d.ts.map +0 -1
  61. package/dist/project.js.map +0 -1
  62. package/dist/serialize.d.ts.map +0 -1
  63. package/dist/serialize.js.map +0 -1
  64. package/dist/sitemap.d.ts.map +0 -1
  65. package/dist/sitemap.js.map +0 -1
  66. package/dist/taxonomy.d.ts.map +0 -1
  67. package/dist/taxonomy.js.map +0 -1
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Redirects — `redirect:` / `aliases:` frontmatter and the `redirects:` config
3
+ * key, turned into rules and then into whatever the deploy target can honour.
4
+ *
5
+ * See plans/redirects-and-aliases.md. Three things drove the design:
6
+ *
7
+ * 1. **`redirect:` was accepted everywhere and emitted nowhere.** The schema
8
+ * took it, `ExportPage` carried it, the sitemap excluded the page and
9
+ * `format-dogsbay-md` round-tripped it — and `format-astro` wrote a normal
10
+ * empty page. Four consumers honouring a field is what made it look
11
+ * supported. So the page skip lives here too, not just the rule.
12
+ *
13
+ * 2. **A rule's paths are SERVED paths.** Cloudflare matches the request path,
14
+ * which carries the mount prefix. `/old → /new` on a site mounted at
15
+ * `/blog` must be written `/blog/old → /blog/new` or it never fires.
16
+ *
17
+ * 3. **Trailing slashes decide whether a redirect costs one hop or two.**
18
+ * Sites serve `/guide/auth/` (docs-dev/url-shape.md). A rule targeting
19
+ * `/guide/auth` redirects to a path that itself redirects. Internal paths
20
+ * are normalized to the served form.
21
+ *
22
+ * This module is pure: it does no IO, so every rule and every refusal is
23
+ * testable without building a site.
24
+ */
25
+ /** One redirect, with its paths already resolved to served form. */
26
+ export interface RedirectRule {
27
+ /** Served path, mount prefix included. Always starts with "/". */
28
+ from: string;
29
+ /** Served path or an absolute external URL. */
30
+ to: string;
31
+ /** 301 for renames (the default), or whatever the author asked for. */
32
+ status: number;
33
+ /** Where the rule came from, for error messages that can be acted on. */
34
+ origin: "frontmatter" | "aliases" | "config";
35
+ /** Page slug that declared it, when it came from a page. */
36
+ declaredBy?: string;
37
+ }
38
+ /** The `redirects:` config block: shorthand string, or an object with a status. */
39
+ export type RedirectsConfig = Record<string, string | {
40
+ to: string;
41
+ status?: number;
42
+ }>;
43
+ export interface CollectRedirectsInput {
44
+ /** Every page the build will emit, for the dangling-target and live-page checks. */
45
+ pages: {
46
+ slug: string;
47
+ redirect?: string;
48
+ frontmatter?: Record<string, unknown>;
49
+ multiSource?: {
50
+ originalSlug?: string;
51
+ };
52
+ }[];
53
+ /** The `redirects:` config block, if any. */
54
+ config?: RedirectsConfig;
55
+ /** URL prefix the site is served under (urlBase + basePath), "" when at the root. */
56
+ prefix: string;
57
+ /** A rule the build cannot honour is left out with a warning instead of failing (`--allow-broken-redirects`). */
58
+ lenient?: boolean;
59
+ }
60
+ export interface CollectRedirectsResult {
61
+ rules: RedirectRule[];
62
+ /** Fatal — the caller throws. A wrong redirect is worse than none. */
63
+ errors: string[];
64
+ /** Non-fatal, printed. Chains are collapsed rather than refused. */
65
+ warnings: string[];
66
+ }
67
+ /**
68
+ * A page slug → the path it is served at. Mirrors the emitter: pages are
69
+ * directories with a trailing slash, `index` is the prefix root.
70
+ */
71
+ export declare function slugToServedPath(slug: string, prefix: string): string;
72
+ /**
73
+ * The site-path prefix a page's source is published under (its namespace,
74
+ * language and version segments, as `latest` or `en/v3`): its slug without
75
+ * the path the source gave it. "" for a site without those axes.
76
+ */
77
+ export declare function pageUnitPrefix(page: {
78
+ slug: string;
79
+ multiSource?: {
80
+ originalSlug?: string;
81
+ };
82
+ }): string;
83
+ /**
84
+ * A path written in a page's front matter (`redirect:`, `aliases:`) as a site
85
+ * path: relative to the page's unit (its language and version), unless it
86
+ * already starts with it, as dogsbay-ssg reads it (issue #096). A copied
87
+ * version (`latest` snapshotted to `v4/`) keeps its redirects inside itself.
88
+ * `mount` (urlBase + basePath) may be written or not; URLs pass through.
89
+ */
90
+ export declare function inPageUnit(raw: string, unit: string, mount: string): string;
91
+ /**
92
+ * Normalize an authored path to the served form: leading slash, mount prefix
93
+ * applied once, trailing slash unless it names a file. External URLs pass
94
+ * through untouched.
95
+ */
96
+ export declare function normalizeTarget(raw: string, prefix: string): string;
97
+ /**
98
+ * Gather every declared redirect, resolve paths, and refuse the ones that
99
+ * would be wrong at runtime. Pure — `errors` is the caller's cue to throw.
100
+ */
101
+ export declare function collectRedirects(input: CollectRedirectsInput): CollectRedirectsResult;
102
+ /** The `_redirects` file Workers Static Assets reads from the assets root. */
103
+ export declare function renderRedirectsFile(rules: RedirectRule[]): string;
104
+ /**
105
+ * A static stub for hosts with no redirect mechanism (GitHub Pages, plain
106
+ * static hosting). This is a genuine downgrade and is reported as one: the
107
+ * response is 200, and the redirect is a hint rather than a status code.
108
+ * `noindex` plus a canonical keeps crawlers pointed at the real page.
109
+ */
110
+ export declare function renderMetaRefreshPage(rule: RedirectRule, siteUrl?: string): string;
111
+ /**
112
+ * Where a stub page goes inside `public/`, relative to it. The served path
113
+ * already carries the mount prefix, but `public/` is copied into the mount
114
+ * directory, so the prefix is stripped back off here — otherwise the stub
115
+ * lands at `<mount>/<mount>/old/`.
116
+ */
117
+ export declare function stubPathForRule(rule: RedirectRule, prefix: string): string;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Redirects — `redirect:` / `aliases:` frontmatter and the `redirects:` config
3
+ * key, turned into rules and then into whatever the deploy target can honour.
4
+ *
5
+ * See plans/redirects-and-aliases.md. Three things drove the design:
6
+ *
7
+ * 1. **`redirect:` was accepted everywhere and emitted nowhere.** The schema
8
+ * took it, `ExportPage` carried it, the sitemap excluded the page and
9
+ * `format-dogsbay-md` round-tripped it — and `format-astro` wrote a normal
10
+ * empty page. Four consumers honouring a field is what made it look
11
+ * supported. So the page skip lives here too, not just the rule.
12
+ *
13
+ * 2. **A rule's paths are SERVED paths.** Cloudflare matches the request path,
14
+ * which carries the mount prefix. `/old → /new` on a site mounted at
15
+ * `/blog` must be written `/blog/old → /blog/new` or it never fires.
16
+ *
17
+ * 3. **Trailing slashes decide whether a redirect costs one hop or two.**
18
+ * Sites serve `/guide/auth/` (docs-dev/url-shape.md). A rule targeting
19
+ * `/guide/auth` redirects to a path that itself redirects. Internal paths
20
+ * are normalized to the served form.
21
+ *
22
+ * This module is pure: it does no IO, so every rule and every refusal is
23
+ * testable without building a site.
24
+ */
25
+ import { isExternalUrl, renderRedirectStub, renderRedirectsFile as renderRedirectsLines, resolveRedirects, } from "@dogsbay/site-files";
26
+ const VALID_STATUS = new Set([301, 302, 303, 307, 308]);
27
+ const isExternal = isExternalUrl;
28
+ /**
29
+ * A page slug → the path it is served at. Mirrors the emitter: pages are
30
+ * directories with a trailing slash, `index` is the prefix root.
31
+ */
32
+ export function slugToServedPath(slug, prefix) {
33
+ const clean = slug.replace(/^\/+|\/+$/g, "").replace(/(^|\/)index$/, "");
34
+ const base = prefix.replace(/\/+$/, "");
35
+ if (!clean)
36
+ return `${base}/`;
37
+ return `${base}/${clean}/`;
38
+ }
39
+ /**
40
+ * The site-path prefix a page's source is published under (its namespace,
41
+ * language and version segments, as `latest` or `en/v3`): its slug without
42
+ * the path the source gave it. "" for a site without those axes.
43
+ */
44
+ export function pageUnitPrefix(page) {
45
+ const clean = (s) => s.replace(/^\/+|\/+$/g, "").replace(/(^|\/)index$/, "");
46
+ const slug = clean(page.slug);
47
+ const inside = clean(page.multiSource?.originalSlug ?? page.slug);
48
+ if (!inside)
49
+ return slug;
50
+ if (slug === inside)
51
+ return "";
52
+ return slug.endsWith(`/${inside}`) ? slug.slice(0, -inside.length - 1) : "";
53
+ }
54
+ /**
55
+ * A path written in a page's front matter (`redirect:`, `aliases:`) as a site
56
+ * path: relative to the page's unit (its language and version), unless it
57
+ * already starts with it, as dogsbay-ssg reads it (issue #096). A copied
58
+ * version (`latest` snapshotted to `v4/`) keeps its redirects inside itself.
59
+ * `mount` (urlBase + basePath) may be written or not; URLs pass through.
60
+ */
61
+ export function inPageUnit(raw, unit, mount) {
62
+ if (!unit || isExternal(raw))
63
+ return raw;
64
+ const t = raw.trim();
65
+ const cut = t.search(/[?#]/);
66
+ const suffix = cut < 0 ? "" : t.slice(cut);
67
+ let p = (cut < 0 ? t : t.slice(0, cut)).replace(/^\/+/, "");
68
+ const m = mount.replace(/^\/+|\/+$/g, "");
69
+ if (m && (p === m || p.startsWith(`${m}/`)))
70
+ p = p.slice(m.length).replace(/^\/+/, "");
71
+ if (!(p === unit || p.startsWith(`${unit}/`)))
72
+ p = p ? `${unit}/${p}` : unit;
73
+ return `/${p}${suffix}`;
74
+ }
75
+ /**
76
+ * Normalize an authored path to the served form: leading slash, mount prefix
77
+ * applied once, trailing slash unless it names a file. External URLs pass
78
+ * through untouched.
79
+ */
80
+ export function normalizeTarget(raw, prefix) {
81
+ if (isExternal(raw))
82
+ return raw;
83
+ // A "#anchor" or "?query" stays on the target as written; only the path is
84
+ // normalized (it used to become "/a#x/", which matched no page).
85
+ const trimmed = raw.trim();
86
+ const cut = trimmed.search(/[?#]/);
87
+ const suffix = cut < 0 ? "" : trimmed.slice(cut);
88
+ let p = cut < 0 ? trimmed : trimmed.slice(0, cut);
89
+ if (!p.startsWith("/"))
90
+ p = `/${p}`;
91
+ const base = prefix.replace(/\/+$/, "");
92
+ if (base && p !== base && !p.startsWith(`${base}/`)) {
93
+ p = `${base}${p}`;
94
+ }
95
+ const last = p.split("/").pop() ?? "";
96
+ if (!last.includes(".") && !p.endsWith("/"))
97
+ p = `${p}/`;
98
+ return p + suffix;
99
+ }
100
+ /**
101
+ * Gather every declared redirect, resolve paths, and refuse the ones that
102
+ * would be wrong at runtime. Pure — `errors` is the caller's cue to throw.
103
+ */
104
+ export function collectRedirects(input) {
105
+ const { pages, config, prefix } = input;
106
+ const errors = [];
107
+ const warnings = [];
108
+ const livePaths = new Set();
109
+ for (const page of pages) {
110
+ // A page that only redirects is not a live page — it is the thing being
111
+ // redirected away from, and emitAstroPages skips it.
112
+ if (!page.redirect)
113
+ livePaths.add(slugToServedPath(page.slug, prefix));
114
+ }
115
+ // Duplicates are resolved (and reported) by resolveRedirects.
116
+ const candidates = [];
117
+ const add = (rule) => {
118
+ candidates.push(rule);
119
+ };
120
+ // 1. Page frontmatter: `redirect:` (this page forwards) and `aliases:` (old
121
+ // paths that forward TO this page).
122
+ for (const page of pages) {
123
+ const here = slugToServedPath(page.slug, prefix);
124
+ // Front matter paths are relative to the page's language and version (#096).
125
+ const unit = pageUnitPrefix(page);
126
+ if (page.redirect) {
127
+ add({
128
+ from: here,
129
+ to: normalizeTarget(inPageUnit(page.redirect, unit, prefix), prefix),
130
+ status: 301,
131
+ origin: "frontmatter",
132
+ declaredBy: page.slug,
133
+ });
134
+ }
135
+ const aliases = page.frontmatter?.aliases;
136
+ if (aliases !== undefined) {
137
+ if (!Array.isArray(aliases) || aliases.some((a) => typeof a !== "string")) {
138
+ errors.push(`aliases: on ${page.slug} must be a list of paths. ` +
139
+ `To fix: use a YAML list of strings.`);
140
+ }
141
+ else {
142
+ for (const alias of aliases) {
143
+ add({
144
+ from: normalizeTarget(inPageUnit(alias, unit, prefix), prefix),
145
+ to: here,
146
+ status: 301,
147
+ origin: "aliases",
148
+ declaredBy: page.slug,
149
+ });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ // 2. Config, which wins over frontmatter for the same source.
155
+ for (const [from, value] of Object.entries(config ?? {})) {
156
+ const to = typeof value === "string" ? value : value.to;
157
+ const status = typeof value === "string" ? 301 : (value.status ?? 301);
158
+ if (typeof to !== "string" || !to.trim()) {
159
+ errors.push(`redirects: entry "${from}" has no target. To fix: give it a path or URL.`);
160
+ continue;
161
+ }
162
+ if (!VALID_STATUS.has(status)) {
163
+ errors.push(`redirects: entry "${from}" has status ${status}. ` +
164
+ `To fix: use one of ${[...VALID_STATUS].join(", ")}.`);
165
+ continue;
166
+ }
167
+ add({
168
+ from: normalizeTarget(from, prefix),
169
+ to: normalizeTarget(to, prefix),
170
+ status,
171
+ origin: "config",
172
+ });
173
+ }
174
+ const resolved = resolveRedirects(candidates, {
175
+ strict: input.lenient !== true,
176
+ // A target's "#anchor" or "?query" is not part of the page it names.
177
+ isPage: (path) => livePaths.has(path.split(/[?#]/)[0]),
178
+ cloudflareLimits: true,
179
+ });
180
+ return {
181
+ rules: resolved.rules,
182
+ errors: [...errors, ...resolved.errors],
183
+ warnings: [...warnings, ...resolved.warnings],
184
+ };
185
+ }
186
+ /** The `_redirects` file Workers Static Assets reads from the assets root. */
187
+ export function renderRedirectsFile(rules) {
188
+ return renderRedirectsLines(rules);
189
+ }
190
+ /**
191
+ * A static stub for hosts with no redirect mechanism (GitHub Pages, plain
192
+ * static hosting). This is a genuine downgrade and is reported as one: the
193
+ * response is 200, and the redirect is a hint rather than a status code.
194
+ * `noindex` plus a canonical keeps crawlers pointed at the real page.
195
+ */
196
+ export function renderMetaRefreshPage(rule, siteUrl) {
197
+ const canonical = siteUrl && !isExternal(rule.to)
198
+ ? `${siteUrl.replace(/\/+$/, "")}${rule.to}`
199
+ : isExternal(rule.to)
200
+ ? rule.to
201
+ : undefined;
202
+ return renderRedirectStub(rule.to, canonical);
203
+ }
204
+ /**
205
+ * Where a stub page goes inside `public/`, relative to it. The served path
206
+ * already carries the mount prefix, but `public/` is copied into the mount
207
+ * directory, so the prefix is stripped back off here — otherwise the stub
208
+ * lands at `<mount>/<mount>/old/`.
209
+ */
210
+ export function stubPathForRule(rule, prefix) {
211
+ const base = prefix.replace(/\/+$/, "");
212
+ let p = rule.from;
213
+ if (base && p.startsWith(base))
214
+ p = p.slice(base.length);
215
+ p = p.replace(/^\/+|\/+$/g, "");
216
+ // The stub is written at this path under public/. resolveRedirects refuses
217
+ // such a source already; this is the last line before a file write, so a
218
+ // `..` that reached it some other way cannot leave the site.
219
+ if (p.split("/").some((seg) => seg === ".." || seg === ".") || /[\\\u0000]/.test(p)) {
220
+ throw new Error(`redirect source ${JSON.stringify(rule.from)} would write outside the site`);
221
+ }
222
+ return p ? `${p}/index.html` : "index.html";
223
+ }
@@ -19,12 +19,25 @@ export interface AstroGenOptions {
19
19
  imageOptimization?: boolean;
20
20
  /** Controls CodeBlock title bar: true = always (default), "auto" = only with explicit title */
21
21
  codeBlockTitle?: boolean | "auto";
22
+ /** A paragraph that is one titled image renders as a captioned figure (`content.imageCaptions: title`). */
23
+ imageCaptions?: boolean;
24
+ /**
25
+ * Combined URL prefix (urlBase + basePath). When non-empty, the
26
+ * caller is expected to have already pre-rewritten absolute image
27
+ * srcs in the tree to include this prefix; the serializer uses
28
+ * the prefix only to strip it back off for the imageMap lookup
29
+ * key (which is keyed off the unprefixed publicPath built via
30
+ * `import.meta.glob` at runtime).
31
+ */
32
+ combinedPrefix?: string;
22
33
  }
34
+ import { TONE_CLASSES } from "./tone-classes.js";
35
+ export { TONE_CLASSES };
23
36
  /** Escape for use inside an Astro {expression}. Uses JSON.stringify for safety. */
24
37
  export declare function escapeExpr(s: string): string;
25
38
  /** Escape for use in an HTML attribute value (double-quoted). */
26
39
  export declare function escapeAttr(s: string): string;
40
+ export declare function componentAttr(name: string, value: string): string;
27
41
  /** Escape text for use directly in an Astro template (not inside an expression). */
28
42
  export declare function escapeTemplate(s: string): string;
29
43
  export declare function treeToAstro(nodes: TreeNode[], options?: AstroGenOptions): AstroGenResult;
30
- //# sourceMappingURL=serialize.d.ts.map