@coywolf/emdash 0.0.0-stage → 0.35.14
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/LICENSE +21 -0
- package/README.md +203 -2
- package/docs/README.md +45 -0
- package/docs/ai.md +39 -0
- package/docs/backups.md +88 -0
- package/docs/breadcrumbs.md +59 -0
- package/docs/code-blocks.md +21 -0
- package/docs/content-urls.md +41 -0
- package/docs/custom-blocks.md +27 -0
- package/docs/discovery.md +32 -0
- package/docs/files.md +44 -0
- package/docs/form-uploads.md +59 -0
- package/docs/headings.md +37 -0
- package/docs/images.md +141 -0
- package/docs/internals.md +13 -0
- package/docs/links.md +34 -0
- package/docs/performance.md +72 -0
- package/docs/redirects.md +84 -0
- package/docs/releasing.md +21 -0
- package/docs/reviews.md +89 -0
- package/docs/robots.md +81 -0
- package/docs/schema.md +79 -0
- package/docs/search.md +114 -0
- package/docs/videos.md +48 -0
- package/docs/wordpress-import.md +198 -0
- package/package.json +79 -5
- package/src/admin/admin-css.ts +57 -0
- package/src/admin/ai.tsx +915 -0
- package/src/admin/backups.tsx +613 -0
- package/src/admin/breadcrumbs.tsx +542 -0
- package/src/admin/codeBlocks.tsx +204 -0
- package/src/admin/customBlocks.tsx +321 -0
- package/src/admin/dirty.ts +20 -0
- package/src/admin/discovery.tsx +463 -0
- package/src/admin/download.ts +28 -0
- package/src/admin/feature-switches.tsx +124 -0
- package/src/admin/features.tsx +142 -0
- package/src/admin/files.tsx +791 -0
- package/src/admin/formUploads.tsx +445 -0
- package/src/admin/guides.tsx +18 -0
- package/src/admin/headings.tsx +262 -0
- package/src/admin/images.tsx +825 -0
- package/src/admin/index.tsx +62 -0
- package/src/admin/links.tsx +980 -0
- package/src/admin/performance.tsx +330 -0
- package/src/admin/preview.tsx +63 -0
- package/src/admin/redirects-removed.tsx +266 -0
- package/src/admin/redirects.tsx +470 -0
- package/src/admin/reviews.tsx +232 -0
- package/src/admin/robots-bots.tsx +647 -0
- package/src/admin/robots-shared.tsx +109 -0
- package/src/admin/robots-wizard.tsx +748 -0
- package/src/admin/robots.tsx +911 -0
- package/src/admin/save-bar.tsx +134 -0
- package/src/admin/schema.tsx +1187 -0
- package/src/admin/search.tsx +355 -0
- package/src/admin/settings-ui.tsx +87 -0
- package/src/admin/trash-prompt-core.ts +135 -0
- package/src/admin/trash-prompt.tsx +156 -0
- package/src/admin/usage.tsx +301 -0
- package/src/admin/videos-display.tsx +248 -0
- package/src/admin/videos.tsx +1202 -0
- package/src/admin/wpImport.tsx +1516 -0
- package/src/ai/entities.ts +59 -0
- package/src/ai/export.ts +61 -0
- package/src/ai/logic.ts +606 -0
- package/src/ai/pack.ts +121 -0
- package/src/ai/prompts.ts +87 -0
- package/src/ai/providers.ts +276 -0
- package/src/ai/routes.ts +359 -0
- package/src/ai/store.ts +301 -0
- package/src/ai/wikidata.ts +87 -0
- package/src/ai/worker.ts +530 -0
- package/src/astro/astro-modules.d.ts +6 -0
- package/src/astro/breadcrumbs/Breadcrumbs.astro +129 -0
- package/src/astro/breadcrumbs/BreadcrumbsBlock.astro +12 -0
- package/src/astro/codeBlocks/CodeBlock.astro +62 -0
- package/src/astro/codeBlocks/EmDashCode.astro +61 -0
- package/src/astro/customBlocks/CustomBlock.astro +64 -0
- package/src/astro/customBlocks/Details.astro +8 -0
- package/src/astro/customBlocks/Disclosure.astro +8 -0
- package/src/astro/customBlocks/Note.astro +8 -0
- package/src/astro/customBlocks/Podcast.astro +8 -0
- package/src/astro/customBlocks/Quote.astro +8 -0
- package/src/astro/customBlocks/Testimonial.astro +8 -0
- package/src/astro/files/FileCard.astro +103 -0
- package/src/astro/headings/HeadingBlock.astro +129 -0
- package/src/astro/headings/TableOfContents.astro +45 -0
- package/src/astro/headings/request.ts +19 -0
- package/src/astro/images/ResponsiveImage.astro +60 -0
- package/src/astro/index.ts +84 -0
- package/src/astro/reviews/CoywolfReview.astro +27 -0
- package/src/astro/reviews/Review.astro +49 -0
- package/src/astro/search/SearchBox.astro +61 -0
- package/src/astro/search/SearchBoxForm.astro +509 -0
- package/src/astro/tables/CaptionedTable.astro +72 -0
- package/src/astro/videos/CoywolfVideo.astro +448 -0
- package/src/backups/download.ts +112 -0
- package/src/backups/dump.ts +288 -0
- package/src/backups/module.ts +276 -0
- package/src/backups/pack.ts +33 -0
- package/src/backups/restore.ts +209 -0
- package/src/backups/store.ts +409 -0
- package/src/breadcrumbs/pack.ts +113 -0
- package/src/breadcrumbs/settings.ts +102 -0
- package/src/breadcrumbs/stamp.ts +40 -0
- package/src/breadcrumbs/trail.ts +123 -0
- package/src/codeBlocks/contrast.ts +179 -0
- package/src/codeBlocks/hljs.d.ts +5 -0
- package/src/codeBlocks/pack.ts +110 -0
- package/src/codeBlocks/render.ts +304 -0
- package/src/codeBlocks/settings.ts +18 -0
- package/src/codeBlocks/themes.generated.ts +304 -0
- package/src/codeBlocks/themes.ts +114 -0
- package/src/core/compose.ts +244 -0
- package/src/core/content-url.ts +888 -0
- package/src/core/csv.ts +119 -0
- package/src/core/d1-batch.ts +65 -0
- package/src/core/entry-docs.ts +175 -0
- package/src/core/features-module.ts +48 -0
- package/src/core/features.ts +392 -0
- package/src/core/generation.ts +88 -0
- package/src/core/guide-content.ts +166 -0
- package/src/core/module.ts +104 -0
- package/src/core/read-failures.ts +57 -0
- package/src/core/request-memo.ts +71 -0
- package/src/core/secrets.ts +83 -0
- package/src/core/settings-enhance.ts +74 -0
- package/src/core/site.ts +36 -0
- package/src/core/storage.ts +27 -0
- package/src/customBlocks/pack.ts +260 -0
- package/src/customBlocks/render.ts +424 -0
- package/src/customBlocks/rich.ts +313 -0
- package/src/customBlocks/settings.ts +46 -0
- package/src/discovery/build.ts +252 -0
- package/src/discovery/feed-build.ts +217 -0
- package/src/discovery/feed.ts +172 -0
- package/src/discovery/indexnow.ts +114 -0
- package/src/discovery/llms.ts +79 -0
- package/src/discovery/markdown.ts +294 -0
- package/src/discovery/memory-cache.ts +97 -0
- package/src/discovery/middleware.ts +186 -0
- package/src/discovery/module.ts +353 -0
- package/src/discovery/news.ts +99 -0
- package/src/discovery/pack.ts +58 -0
- package/src/discovery/settings.ts +91 -0
- package/src/discovery/urls.ts +35 -0
- package/src/env.d.ts +10 -0
- package/src/files/card.ts +231 -0
- package/src/files/counts.ts +60 -0
- package/src/files/export.ts +44 -0
- package/src/files/format.ts +163 -0
- package/src/files/module.ts +596 -0
- package/src/files/pack.ts +92 -0
- package/src/files/r2-rules.ts +41 -0
- package/src/files/range.ts +50 -0
- package/src/files/s3.ts +114 -0
- package/src/files/serve.ts +141 -0
- package/src/files/sigv4.ts +160 -0
- package/src/files/site.ts +137 -0
- package/src/files/walker.ts +58 -0
- package/src/formUploads/lib.ts +266 -0
- package/src/formUploads/pack.ts +55 -0
- package/src/formUploads/wrap.ts +592 -0
- package/src/forms.ts +16 -0
- package/src/headings/pack.ts +154 -0
- package/src/headings/render.ts +139 -0
- package/src/headings/settings.ts +117 -0
- package/src/headings/slug.ts +68 -0
- package/src/headings/stamp.ts +142 -0
- package/src/headings/test-hooks.mjs +14 -0
- package/src/headings/toc.ts +48 -0
- package/src/images/cloudflare.ts +329 -0
- package/src/images/lib.ts +221 -0
- package/src/images/module.ts +372 -0
- package/src/images/pack.ts +804 -0
- package/src/images/pending.ts +37 -0
- package/src/images/settings.ts +48 -0
- package/src/images/setup.ts +192 -0
- package/src/images/variants-estimate.ts +162 -0
- package/src/images/variants-store.ts +475 -0
- package/src/images/variants.ts +311 -0
- package/src/index.ts +143 -0
- package/src/links/check.ts +101 -0
- package/src/links/classify.ts +270 -0
- package/src/links/export.ts +59 -0
- package/src/links/module.ts +652 -0
- package/src/links/pack.ts +43 -0
- package/src/links/pt.ts +325 -0
- package/src/links/store.ts +704 -0
- package/src/middleware.ts +287 -0
- package/src/modules.ts +53 -0
- package/src/options.ts +66 -0
- package/src/pageCache/lib.ts +274 -0
- package/src/pageCache/pack.ts +240 -0
- package/src/pageCache/usage-core.ts +152 -0
- package/src/pageCache/usage.ts +262 -0
- package/src/pageCache/warm.ts +722 -0
- package/src/redirects/middleware.ts +169 -0
- package/src/redirects/module.ts +124 -0
- package/src/redirects/pack.ts +89 -0
- package/src/redirects/removed-core.ts +151 -0
- package/src/redirects/removed.ts +293 -0
- package/src/redirects/rules.ts +355 -0
- package/src/redirects/slug-changes.ts +207 -0
- package/src/redirects/slug-core.ts +49 -0
- package/src/redirects/transfer.ts +92 -0
- package/src/reviews/lib.ts +624 -0
- package/src/reviews/pack.ts +135 -0
- package/src/reviews/schema.ts +128 -0
- package/src/reviews/settings.ts +26 -0
- package/src/robots/bots.ts +321 -0
- package/src/robots/data/bots.json +696 -0
- package/src/robots/data/verified.json +582 -0
- package/src/robots/directory.ts +26 -0
- package/src/robots/explain.ts +191 -0
- package/src/robots/guided.ts +300 -0
- package/src/robots/importer.ts +263 -0
- package/src/robots/middleware.ts +151 -0
- package/src/robots/module.ts +446 -0
- package/src/robots/pack.ts +69 -0
- package/src/robots/radar.ts +129 -0
- package/src/robots/rep.ts +362 -0
- package/src/robots/rules.ts +883 -0
- package/src/robots/transfer.ts +45 -0
- package/src/robots/validate.ts +524 -0
- package/src/schema/catalog.ts +146 -0
- package/src/schema/graph.ts +791 -0
- package/src/schema/module.ts +1138 -0
- package/src/schema/og-image.ts +100 -0
- package/src/schema/pack.ts +58 -0
- package/src/search/engine.ts +578 -0
- package/src/search/fallback.ts +80 -0
- package/src/search/live-cache.ts +116 -0
- package/src/search/live-client.ts +770 -0
- package/src/search/live-serve.ts +282 -0
- package/src/search/module.ts +261 -0
- package/src/search/pack.ts +57 -0
- package/src/search/query.ts +132 -0
- package/src/search/ratelimit-core.ts +118 -0
- package/src/search/ratelimit.ts +79 -0
- package/src/search/snippet.ts +135 -0
- package/src/search/title-index.ts +139 -0
- package/src/search/title-match.ts +192 -0
- package/src/shared.ts +71 -0
- package/src/version.ts +2 -0
- package/src/videos/date.ts +40 -0
- package/src/videos/engagement.ts +75 -0
- package/src/videos/lib.ts +679 -0
- package/src/videos/module.ts +1160 -0
- package/src/videos/pack.ts +187 -0
- package/src/videos/pause.ts +42 -0
- package/src/videos/poster.ts +324 -0
- package/src/videos/render.ts +336 -0
- package/src/videos/store.ts +551 -0
- package/src/videos/stream.ts +134 -0
- package/src/wpImport/convert.ts +702 -0
- package/src/wpImport/guests.ts +329 -0
- package/src/wpImport/gutenberg.ts +157 -0
- package/src/wpImport/markers.ts +66 -0
- package/src/wpImport/pack.ts +451 -0
- package/src/wpImport/parents.ts +202 -0
- package/src/wpImport/prepare.ts +592 -0
- package/src/wpImport/redirects.ts +358 -0
- package/src/wpImport/sources.ts +67 -0
- package/src/wpImport/stream.ts +160 -0
- package/src/wpImport/urls.ts +354 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Accessible breadcrumb trail (WAI-ARIA breadcrumb pattern): <nav
|
|
4
|
+
* aria-label="Breadcrumb"> around an <ol>, links for ancestors, and the
|
|
5
|
+
* current page as plain text with aria-current="page". Separators are CSS, so
|
|
6
|
+
* screen readers skip them.
|
|
7
|
+
*
|
|
8
|
+
* The trail is, in order: `items`, `page.breadcrumbs` (the theme's
|
|
9
|
+
* PublicPageContext), the trail EmDashHead published for this path, or one
|
|
10
|
+
* derived from the URL path. Unset options use the Breadcrumb Nav settings.
|
|
11
|
+
* Renders nothing while the `breadcrumbs` feature is off.
|
|
12
|
+
*
|
|
13
|
+
* import { Breadcrumbs } from "@coywolf/emdash/astro";
|
|
14
|
+
* <Breadcrumbs page={page} />
|
|
15
|
+
*/
|
|
16
|
+
import { siteFeatureOn } from "../../core/features.js";
|
|
17
|
+
import { SEPARATORS, type SeparatorPreset, siteBreadcrumbsSettings } from "../../breadcrumbs/settings.js";
|
|
18
|
+
import { type Crumb, capturedPage, cssString, resolveTrail } from "../../breadcrumbs/trail.js";
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
items?: Crumb[];
|
|
22
|
+
page?: { breadcrumbs?: Crumb[]; pageTitle?: string | null; title?: string | null };
|
|
23
|
+
/** Current page title, when neither `items` nor `page` gives one. */
|
|
24
|
+
title?: string | null;
|
|
25
|
+
/** A preset name (slash, chevron, guillemet, bullet, arrow, gt) or any short string. */
|
|
26
|
+
separator?: string;
|
|
27
|
+
homeLabel?: string;
|
|
28
|
+
showHome?: boolean;
|
|
29
|
+
showCurrent?: boolean;
|
|
30
|
+
class?: string;
|
|
31
|
+
label?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const props = Astro.props;
|
|
35
|
+
let trail: Crumb[] = [];
|
|
36
|
+
let sepClass = "";
|
|
37
|
+
let sepStyle: string | undefined;
|
|
38
|
+
|
|
39
|
+
if (await siteFeatureOn("breadcrumbs")) {
|
|
40
|
+
const settings = await siteBreadcrumbsSettings();
|
|
41
|
+
const captured = props.items || props.page ? null : capturedPage(`${Astro.url.pathname}${Astro.url.search}`);
|
|
42
|
+
trail = resolveTrail({
|
|
43
|
+
path: Astro.url.pathname,
|
|
44
|
+
items: props.items ?? props.page?.breadcrumbs ?? captured?.items,
|
|
45
|
+
title: props.page?.pageTitle ?? props.page?.title ?? props.title ?? captured?.title,
|
|
46
|
+
homeLabel: props.homeLabel?.trim() || settings.homeLabel,
|
|
47
|
+
showHome: props.showHome ?? settings.showHome,
|
|
48
|
+
showCurrent: props.showCurrent ?? settings.showCurrent,
|
|
49
|
+
});
|
|
50
|
+
const separator = props.separator ?? (settings.customSeparator || settings.separator);
|
|
51
|
+
if (Object.hasOwn(SEPARATORS, separator)) sepClass = `cw-breadcrumbs--sep-${separator as SeparatorPreset}`;
|
|
52
|
+
else sepStyle = `--cw-bc-sep:${cssString([...separator].slice(0, 8).join(""))}`;
|
|
53
|
+
}
|
|
54
|
+
const current = Astro.url.pathname.replace(/\/+$/, "") || "/";
|
|
55
|
+
const isCurrent = (url: string) => {
|
|
56
|
+
try {
|
|
57
|
+
return (new URL(url, Astro.url).pathname.replace(/\/+$/, "") || "/") === current;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
{trail.length > 0 && (
|
|
65
|
+
<nav class={["cw-breadcrumbs", sepClass, props.class].filter(Boolean).join(" ")} style={sepStyle} aria-label={props.label ?? "Breadcrumb"}>
|
|
66
|
+
<ol class="cw-breadcrumbs__list">
|
|
67
|
+
{trail.map((crumb, i) => (
|
|
68
|
+
<li class="cw-breadcrumbs__item">
|
|
69
|
+
{i === trail.length - 1 && isCurrent(crumb.url) ? (
|
|
70
|
+
<span class="cw-breadcrumbs__current" aria-current="page">{crumb.name}</span>
|
|
71
|
+
) : (
|
|
72
|
+
<a href={crumb.url}>{crumb.name}</a>
|
|
73
|
+
)}
|
|
74
|
+
</li>
|
|
75
|
+
))}
|
|
76
|
+
</ol>
|
|
77
|
+
</nav>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
<style is:global>
|
|
81
|
+
.cw-breadcrumbs {
|
|
82
|
+
--cw-bc-sep: "/";
|
|
83
|
+
font-size: 0.875em;
|
|
84
|
+
line-height: 1.4;
|
|
85
|
+
}
|
|
86
|
+
.cw-breadcrumbs__list {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-wrap: wrap;
|
|
89
|
+
align-items: center;
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding: 0;
|
|
92
|
+
list-style: none;
|
|
93
|
+
}
|
|
94
|
+
.cw-breadcrumbs__item {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
}
|
|
98
|
+
.cw-breadcrumbs__item + .cw-breadcrumbs__item::before {
|
|
99
|
+
content: var(--cw-bc-sep, "/");
|
|
100
|
+
padding: 0 0.5em;
|
|
101
|
+
opacity: 0.6;
|
|
102
|
+
}
|
|
103
|
+
@supports (content: "x" / "") {
|
|
104
|
+
.cw-breadcrumbs__item + .cw-breadcrumbs__item::before {
|
|
105
|
+
content: var(--cw-bc-sep, "/") / "";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
.cw-breadcrumbs__current {
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
}
|
|
111
|
+
.cw-breadcrumbs--sep-slash {
|
|
112
|
+
--cw-bc-sep: "/";
|
|
113
|
+
}
|
|
114
|
+
.cw-breadcrumbs--sep-chevron {
|
|
115
|
+
--cw-bc-sep: "\203A";
|
|
116
|
+
}
|
|
117
|
+
.cw-breadcrumbs--sep-guillemet {
|
|
118
|
+
--cw-bc-sep: "\00BB";
|
|
119
|
+
}
|
|
120
|
+
.cw-breadcrumbs--sep-bullet {
|
|
121
|
+
--cw-bc-sep: "\2022";
|
|
122
|
+
}
|
|
123
|
+
.cw-breadcrumbs--sep-arrow {
|
|
124
|
+
--cw-bc-sep: "\2192";
|
|
125
|
+
}
|
|
126
|
+
.cw-breadcrumbs--sep-gt {
|
|
127
|
+
--cw-bc-sep: ">";
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** The `coywolf-breadcrumbs` Portable Text block: the Breadcrumbs component with the block's options. */
|
|
3
|
+
import Breadcrumbs from "./Breadcrumbs.astro";
|
|
4
|
+
|
|
5
|
+
const { node } = Astro.props as { node: Record<string, unknown> };
|
|
6
|
+
const separator = typeof node.separator === "string" && node.separator ? node.separator : undefined;
|
|
7
|
+
const homeLabel = typeof node.homeLabel === "string" && node.homeLabel.trim() ? node.homeLabel : undefined;
|
|
8
|
+
const showCurrent = node.showCurrent === "show" ? true : node.showCurrent === "hide" ? false : undefined;
|
|
9
|
+
const title = typeof node._title === "string" ? node._title : undefined;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<Breadcrumbs separator={separator} homeLabel={homeLabel} showCurrent={showCurrent} title={title} />
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Portable Text `code` block, replacing EmDash's built-in renderer when the
|
|
4
|
+
* Code blocks feature is on. Highlighting runs here on the server (lowlight);
|
|
5
|
+
* the chrome CSS, the active theme's CSS and the tiny copy script are inlined
|
|
6
|
+
* once per page, by the first code block, so pages without code get nothing.
|
|
7
|
+
* When the feature is off this renders EmDash's own code block markup
|
|
8
|
+
* (EmDashCode.astro).
|
|
9
|
+
*/
|
|
10
|
+
import { siteFeatures } from "../../core/features.js";
|
|
11
|
+
import EmDashCode from "./EmDashCode.astro";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
node: { _type: "code"; _key: string; code: string; language?: string; filename?: string };
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { node } = Astro.props;
|
|
19
|
+
|
|
20
|
+
// Claim the per-page assets synchronously, before any await, so the first
|
|
21
|
+
// block in document order carries them even when siblings render in parallel.
|
|
22
|
+
const locals = (Astro.locals ?? {}) as Record<string, unknown>;
|
|
23
|
+
const hasCode = Boolean(node?.code);
|
|
24
|
+
const firstBlock = hasCode && !locals.__cwCodeAssets;
|
|
25
|
+
if (hasCode) locals.__cwCodeAssets = true;
|
|
26
|
+
|
|
27
|
+
const features = await siteFeatures();
|
|
28
|
+
const on = hasCode && features.codeBlocks === true;
|
|
29
|
+
const copy = features["codeBlocks.copy"] === true;
|
|
30
|
+
|
|
31
|
+
let html = "";
|
|
32
|
+
let css = "";
|
|
33
|
+
let script = "";
|
|
34
|
+
if (on) {
|
|
35
|
+
// Loaded only when the feature is on, so the grammars and themes stay out of
|
|
36
|
+
// the request path otherwise.
|
|
37
|
+
const [{ CHROME_CSS, COPY_SCRIPT, renderBlock }, { siteTheme }, { themeCss }] = await Promise.all([
|
|
38
|
+
import("../../codeBlocks/render.js"),
|
|
39
|
+
import("../../codeBlocks/settings.js"),
|
|
40
|
+
import("../../codeBlocks/themes.js"),
|
|
41
|
+
]);
|
|
42
|
+
html = renderBlock(node, {
|
|
43
|
+
label: features["codeBlocks.label"] === true,
|
|
44
|
+
copy,
|
|
45
|
+
lineNumbers: features["codeBlocks.lineNumbers"] === true,
|
|
46
|
+
});
|
|
47
|
+
if (firstBlock) {
|
|
48
|
+
css = CHROME_CSS + themeCss(await siteTheme());
|
|
49
|
+
if (copy) script = COPY_SCRIPT;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
{on ? (
|
|
55
|
+
<>
|
|
56
|
+
{css && <style is:inline set:html={css} />}
|
|
57
|
+
<Fragment set:html={html} />
|
|
58
|
+
{script && <script is:inline set:html={script} />}
|
|
59
|
+
</>
|
|
60
|
+
) : (
|
|
61
|
+
<EmDashCode {...Astro.props} />
|
|
62
|
+
)}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Verbatim copy of EmDash's built-in code block renderer (1.1, unchanged in 1.2)
|
|
4
|
+
* (emdash/src/components/Code.astro, MIT), rendered when the Code blocks
|
|
5
|
+
* feature is off. Copied rather than imported from "emdash/ui" because that
|
|
6
|
+
* barrel imports PortableText, which imports this plugin's block components:
|
|
7
|
+
* an import cycle we keep out of the production bundle.
|
|
8
|
+
*/
|
|
9
|
+
export interface Props {
|
|
10
|
+
node: {
|
|
11
|
+
_type: "code";
|
|
12
|
+
_key: string;
|
|
13
|
+
code: string;
|
|
14
|
+
language?: string;
|
|
15
|
+
filename?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { node } = Astro.props;
|
|
20
|
+
|
|
21
|
+
if (!node?.code) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { code, language, filename } = node;
|
|
26
|
+
const languageClass = language ? `language-${language}` : "";
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<div class="emdash-code">
|
|
30
|
+
{filename && <div class="emdash-code-filename">{filename}</div>}
|
|
31
|
+
<pre class={languageClass}><code class={languageClass}>{code}</code></pre>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.emdash-code {
|
|
36
|
+
margin: 1.5rem 0;
|
|
37
|
+
}
|
|
38
|
+
.emdash-code-filename {
|
|
39
|
+
font-size: 0.75rem;
|
|
40
|
+
font-family: monospace;
|
|
41
|
+
background: #2d2d2d;
|
|
42
|
+
color: #ccc;
|
|
43
|
+
padding: 0.5rem 1rem;
|
|
44
|
+
border-radius: 0.5rem 0.5rem 0 0;
|
|
45
|
+
}
|
|
46
|
+
.emdash-code pre {
|
|
47
|
+
margin: 0;
|
|
48
|
+
padding: 1rem;
|
|
49
|
+
background: #1e1e1e;
|
|
50
|
+
border-radius: 0.5rem;
|
|
51
|
+
overflow-x: auto;
|
|
52
|
+
}
|
|
53
|
+
.emdash-code-filename + pre {
|
|
54
|
+
border-radius: 0 0 0.5rem 0.5rem;
|
|
55
|
+
}
|
|
56
|
+
.emdash-code code {
|
|
57
|
+
font-family: "Fira Code", "Consolas", monospace;
|
|
58
|
+
font-size: 0.875rem;
|
|
59
|
+
color: #e0e0e0;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Renders one Custom Blocks block (Note, Details, Affiliate disclosure,
|
|
4
|
+
* Quote, Testimonial, Podcast links) when its feature is on; nothing
|
|
5
|
+
* otherwise. The shared CSS goes out once per page, before the first block.
|
|
6
|
+
* Markup comes from customBlocks/render.ts, which the admin previews use too.
|
|
7
|
+
* Rendered here (not as an HTML block) so <details>, <aside> and <figure>
|
|
8
|
+
* aren't stripped by EmDash's HTML sanitizer.
|
|
9
|
+
*/
|
|
10
|
+
import { siteFeatureOn } from "../../core/features.js";
|
|
11
|
+
import { cleanImageUrl, croppedImage } from "../../images/pack.js";
|
|
12
|
+
import {
|
|
13
|
+
CUSTOM_BLOCKS_CSS,
|
|
14
|
+
blockId,
|
|
15
|
+
normalizeDetails,
|
|
16
|
+
normalizeDisclosure,
|
|
17
|
+
normalizeNote,
|
|
18
|
+
normalizePodcast,
|
|
19
|
+
normalizeQuote,
|
|
20
|
+
normalizeTestimonial,
|
|
21
|
+
renderDetailsHtml,
|
|
22
|
+
renderDisclosureHtml,
|
|
23
|
+
renderNoteHtml,
|
|
24
|
+
renderPodcastHtml,
|
|
25
|
+
renderQuoteHtml,
|
|
26
|
+
renderTestimonialHtml,
|
|
27
|
+
} from "../../customBlocks/render.js";
|
|
28
|
+
import { siteDisclosureSettings, sitePodcastSettings } from "../../customBlocks/settings.js";
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
kind: "note" | "details" | "disclosure" | "quote" | "testimonial" | "podcast";
|
|
32
|
+
node: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { kind, node } = Astro.props as Props;
|
|
36
|
+
|
|
37
|
+
// Claim the per-page CSS synchronously, before any await, so the first block
|
|
38
|
+
// in document order carries it even when siblings render in parallel.
|
|
39
|
+
const locals = (Astro.locals ?? {}) as Record<string, unknown>;
|
|
40
|
+
const first = !locals.__cwCustomBlocksCss;
|
|
41
|
+
locals.__cwCustomBlocksCss = true;
|
|
42
|
+
|
|
43
|
+
let html = "";
|
|
44
|
+
if (node && (await siteFeatureOn(`customBlocks.${kind}`))) {
|
|
45
|
+
if (kind === "note") html = renderNoteHtml(normalizeNote(node), blockId("cw-note", node._key));
|
|
46
|
+
else if (kind === "details") html = renderDetailsHtml(normalizeDetails(node));
|
|
47
|
+
else if (kind === "disclosure") html = renderDisclosureHtml(normalizeDisclosure(node), await siteDisclosureSettings());
|
|
48
|
+
else if (kind === "testimonial") {
|
|
49
|
+
// The photo shows at 64px: small squares from the media host (2x for sharp screens), not the original.
|
|
50
|
+
// Stored crops (64x64 and 128x128 in the site's images.crops) when they exist, else resized by the host.
|
|
51
|
+
const t = normalizeTestimonial(node);
|
|
52
|
+
const stored = t.photo ? await croppedImage(t.photo, [[64, 64, "1x"], [128, 128, "2x"]], { locals }) : null;
|
|
53
|
+
const [x1, x2] = t.photo && !stored ? await Promise.all([cleanImageUrl(t.photo, { width: 64, height: 64 }), cleanImageUrl(t.photo, { width: 128, height: 128 })]) : [null, null];
|
|
54
|
+
html = renderTestimonialHtml(t, stored ? { src: stored.src, srcset: stored.webp, avif: stored.avif } : x1 ? { src: x1, srcset: x2 ? `${x1} 1x, ${x2} 2x` : undefined } : null);
|
|
55
|
+
}
|
|
56
|
+
else if (kind === "podcast") html = renderPodcastHtml(normalizePodcast(node), await sitePodcastSettings(), blockId("cw-podcast", node._key));
|
|
57
|
+
else html = renderQuoteHtml(normalizeQuote(node));
|
|
58
|
+
}
|
|
59
|
+
if (!html && first) locals.__cwCustomBlocksCss = false;
|
|
60
|
+
const css = html && first ? CUSTOM_BLOCKS_CSS : "";
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
{css && <style is:inline set:html={css} />}
|
|
64
|
+
{html && <Fragment set:html={html} />}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Download card for the coywolf-file Portable Text block: file-type badge,
|
|
4
|
+
* name, "TYPE · size · Uploaded date", Download and Copy link. Renders
|
|
5
|
+
* nothing when File Downloads is off or the file no longer exists. Markup and
|
|
6
|
+
* CSS come from files/card.ts, which the admin preview uses too.
|
|
7
|
+
*/
|
|
8
|
+
import { siteFeatureOn } from "../../core/features.js";
|
|
9
|
+
import { noteReadFailure } from "../../core/read-failures.js";
|
|
10
|
+
import { workerEnv } from "../../shared.js";
|
|
11
|
+
import { FILE_CSS, renderFileCardHtml } from "../../files/card.js";
|
|
12
|
+
import { downloadPath } from "../../files/format.js";
|
|
13
|
+
import { readSiteSettings, resolveFile } from "../../files/site.js";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
node: {
|
|
17
|
+
_key?: string;
|
|
18
|
+
id?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
showIcon?: boolean;
|
|
22
|
+
showDescription?: boolean;
|
|
23
|
+
showMeta?: boolean;
|
|
24
|
+
showDownload?: boolean;
|
|
25
|
+
showCopyLink?: boolean;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { node } = Astro.props;
|
|
30
|
+
|
|
31
|
+
let html = "";
|
|
32
|
+
if (node?.id && (await siteFeatureOn("files"))) {
|
|
33
|
+
try {
|
|
34
|
+
const env = await workerEnv();
|
|
35
|
+
const db = env.DB as D1Database | undefined;
|
|
36
|
+
const file = db ? await resolveFile(db, String(node.id)) : null;
|
|
37
|
+
if (db && file) {
|
|
38
|
+
const settings = await readSiteSettings(db);
|
|
39
|
+
const href = downloadPath(settings.base, file.id, file.name);
|
|
40
|
+
html = renderFileCardHtml(
|
|
41
|
+
{
|
|
42
|
+
title: node.title?.trim() || file.name,
|
|
43
|
+
description: node.description?.trim() ?? "",
|
|
44
|
+
name: file.name,
|
|
45
|
+
size: file.size,
|
|
46
|
+
uploadedAt: file.uploadedAt,
|
|
47
|
+
href,
|
|
48
|
+
absolute: new URL(href, Astro.url).href,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
scheme: settings.scheme,
|
|
52
|
+
accent: settings.accent,
|
|
53
|
+
showIcon: node.showIcon,
|
|
54
|
+
showDescription: node.showDescription,
|
|
55
|
+
showMeta: node.showMeta,
|
|
56
|
+
showDownload: node.showDownload,
|
|
57
|
+
showCopyLink: node.showCopyLink,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("coywolf-pack files: could not render a download card", error);
|
|
63
|
+
// The page goes out without its card: not into the edge cache.
|
|
64
|
+
noteReadFailure("file card");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The CSS goes out once per page, before the first card.
|
|
69
|
+
const locals = (Astro.locals ?? {}) as Record<string, unknown>;
|
|
70
|
+
const css = html && !locals.__cwFileCss ? FILE_CSS : "";
|
|
71
|
+
if (css) locals.__cwFileCss = true;
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
{css && <style is:inline set:html={css} />}
|
|
75
|
+
{html && <Fragment set:html={html} />}
|
|
76
|
+
|
|
77
|
+
<script>
|
|
78
|
+
// Copy link: clipboard, checkmark swap, and a polite announcement. One delegated listener per page.
|
|
79
|
+
const timers = new WeakMap<Element, number>();
|
|
80
|
+
document.addEventListener("click", async (event) => {
|
|
81
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
82
|
+
const button = target?.closest<HTMLButtonElement>(".cw-file__copy");
|
|
83
|
+
if (!button) return;
|
|
84
|
+
const url = button.dataset.cwFileUrl;
|
|
85
|
+
const status = button.closest(".cw-file")?.querySelector(".cw-file__sr");
|
|
86
|
+
if (!url) return;
|
|
87
|
+
try {
|
|
88
|
+
await navigator.clipboard.writeText(url);
|
|
89
|
+
button.classList.add("is-copied");
|
|
90
|
+
if (status) status.textContent = "Link copied";
|
|
91
|
+
clearTimeout(timers.get(button));
|
|
92
|
+
timers.set(
|
|
93
|
+
button,
|
|
94
|
+
window.setTimeout(() => {
|
|
95
|
+
button.classList.remove("is-copied");
|
|
96
|
+
if (status) status.textContent = "";
|
|
97
|
+
}, 2000),
|
|
98
|
+
);
|
|
99
|
+
} catch {
|
|
100
|
+
if (status) status.textContent = "Could not copy the link.";
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Portable Text `block` override that gives H2–H6 headings their anchor id.
|
|
4
|
+
* EmDash's PortableText only lets plugins add custom block *types*, so a
|
|
5
|
+
* theme opts in by passing this as the `block` component:
|
|
6
|
+
*
|
|
7
|
+
* import { portableTextComponents } from "@coywolf/emdash/astro";
|
|
8
|
+
* <PortableText value={...} components={portableTextComponents} />
|
|
9
|
+
*
|
|
10
|
+
* Every block is rendered by EmDash's own Block component (same markup,
|
|
11
|
+
* text-align classes included); headings additionally get `id` (the anchor
|
|
12
|
+
* stamped on save, or one derived from the text for content saved before the
|
|
13
|
+
* feature was on), an optional scroll margin, and an optional copy-link.
|
|
14
|
+
*/
|
|
15
|
+
import { isOn, siteFeatures } from "../../core/features.js";
|
|
16
|
+
import { siteHeadingsSettings } from "../../headings/settings.js";
|
|
17
|
+
import { blockText, headingLevel, uniqueSlug, validAnchor } from "../../headings/slug.js";
|
|
18
|
+
import { firstInRequest, takenIds } from "./request.js";
|
|
19
|
+
|
|
20
|
+
// Imported at render time: emdash/ui imports PortableText, which imports this module.
|
|
21
|
+
const { Block } = await import("emdash/ui");
|
|
22
|
+
|
|
23
|
+
const props = Astro.props as Record<string, unknown> & { node: Record<string, unknown> };
|
|
24
|
+
const node = props.node;
|
|
25
|
+
const level = headingLevel(node);
|
|
26
|
+
|
|
27
|
+
let id: string | undefined;
|
|
28
|
+
let style: string | undefined;
|
|
29
|
+
let copyLink = false;
|
|
30
|
+
let copyScript = false;
|
|
31
|
+
let text = "";
|
|
32
|
+
|
|
33
|
+
if (level && level >= 2) {
|
|
34
|
+
const features = await siteFeatures();
|
|
35
|
+
if (isOn(features, "headings.anchors") || isOn(features, "headings.toc")) {
|
|
36
|
+
const settings = await siteHeadingsSettings();
|
|
37
|
+
const taken = takenIds(Astro.request);
|
|
38
|
+
text = blockText(node);
|
|
39
|
+
if (validAnchor(node.anchor)) {
|
|
40
|
+
id = node.anchor;
|
|
41
|
+
taken.add(id);
|
|
42
|
+
} else if (text) {
|
|
43
|
+
id = uniqueSlug(text, taken, settings.prefix);
|
|
44
|
+
}
|
|
45
|
+
if (id && settings.scrollOffset > 0) style = `scroll-margin-top:${settings.scrollOffset}${settings.scrollUnit}`;
|
|
46
|
+
copyLink = Boolean(id) && isOn(features, "headings.anchors") && settings.copyLink;
|
|
47
|
+
copyScript = copyLink && firstInRequest(Astro.request, "copy-link");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// With a copy-link inside the heading, the heading is named by its text alone
|
|
51
|
+
// (aria-labelledby), so headings lists don't read "Copy link to section" in every name.
|
|
52
|
+
const labelId = copyLink ? `cw-hl-${id}` : undefined;
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
{copyLink ? (
|
|
56
|
+
<Block {...props} id={id} style={style} aria-labelledby={labelId}><span id={labelId}><slot /></span><a class="cw-heading-link" href={`#${id}`}><span class="cw-sr">Copy link to section: {text}</span><span aria-hidden="true">#</span></a></Block>
|
|
57
|
+
) : (
|
|
58
|
+
<Block {...props} id={id} style={style}><slot /></Block>
|
|
59
|
+
)}
|
|
60
|
+
{copyScript && (
|
|
61
|
+
<script is:inline>
|
|
62
|
+
(() => {
|
|
63
|
+
if (window.__cwCopyLink) return;
|
|
64
|
+
window.__cwCopyLink = true;
|
|
65
|
+
let live;
|
|
66
|
+
document.addEventListener("click", (event) => {
|
|
67
|
+
const link = event.target instanceof Element ? event.target.closest("a.cw-heading-link") : null;
|
|
68
|
+
if (!link || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !navigator.clipboard) return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
const url = new URL(link.getAttribute("href"), location.href).href;
|
|
71
|
+
navigator.clipboard.writeText(url).then(
|
|
72
|
+
() => {
|
|
73
|
+
history.replaceState(null, "", url);
|
|
74
|
+
if (!live) {
|
|
75
|
+
live = document.createElement("div");
|
|
76
|
+
live.className = "cw-sr";
|
|
77
|
+
live.setAttribute("aria-live", "polite");
|
|
78
|
+
document.body.append(live);
|
|
79
|
+
}
|
|
80
|
+
live.textContent = "";
|
|
81
|
+
setTimeout(() => (live.textContent = "Link copied"), 50);
|
|
82
|
+
link.setAttribute("data-copied", "");
|
|
83
|
+
setTimeout(() => link.removeAttribute("data-copied"), 1500);
|
|
84
|
+
},
|
|
85
|
+
() => (location.hash = link.hash),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
})();
|
|
89
|
+
</script>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
<style is:global>
|
|
93
|
+
.cw-sr {
|
|
94
|
+
position: absolute;
|
|
95
|
+
width: 1px;
|
|
96
|
+
height: 1px;
|
|
97
|
+
padding: 0;
|
|
98
|
+
margin: -1px;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
clip: rect(0, 0, 0, 0);
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
border: 0;
|
|
103
|
+
}
|
|
104
|
+
.cw-heading-link {
|
|
105
|
+
margin-inline-start: 0.35em;
|
|
106
|
+
font-weight: 400;
|
|
107
|
+
text-decoration: none;
|
|
108
|
+
opacity: 0;
|
|
109
|
+
transition: opacity 0.15s;
|
|
110
|
+
}
|
|
111
|
+
:is(h2, h3, h4, h5, h6):hover > .cw-heading-link,
|
|
112
|
+
.cw-heading-link:focus-visible,
|
|
113
|
+
.cw-heading-link[data-copied] {
|
|
114
|
+
opacity: 0.6;
|
|
115
|
+
}
|
|
116
|
+
.cw-heading-link[data-copied]::after {
|
|
117
|
+
content: " \2713";
|
|
118
|
+
}
|
|
119
|
+
@media (hover: none) {
|
|
120
|
+
.cw-heading-link {
|
|
121
|
+
opacity: 0.6;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
@media (prefers-reduced-motion: reduce) {
|
|
125
|
+
.cw-heading-link {
|
|
126
|
+
transition: none;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The `coywolf-toc` Portable Text block. Lists the headings stamped into the
|
|
4
|
+
* block when the entry was saved (`_headings`), so it always matches the
|
|
5
|
+
* anchors on the page. Collapsing uses <details> (no JS); smooth scrolling is
|
|
6
|
+
* CSS and is skipped for visitors who prefer reduced motion. Markup and CSS
|
|
7
|
+
* come from headings/render.ts, which the admin preview uses too.
|
|
8
|
+
*/
|
|
9
|
+
import { siteFeatureOn } from "../../core/features.js";
|
|
10
|
+
import { type TocDisplay, type TocListStyle, parseLevels, siteHeadingsSettings } from "../../headings/settings.js";
|
|
11
|
+
import { TOC_CSS, renderTocHtml, tocTitleTag } from "../../headings/render.js";
|
|
12
|
+
import { buildTocTree, countToc, storedHeadings } from "../../headings/toc.js";
|
|
13
|
+
|
|
14
|
+
const { node } = Astro.props as { node: Record<string, unknown> };
|
|
15
|
+
|
|
16
|
+
let html = "";
|
|
17
|
+
const titleId = `cw-toc-${String(node._key ?? "").replace(/[^A-Za-z0-9_-]/g, "") || "title"}`;
|
|
18
|
+
|
|
19
|
+
if (await siteFeatureOn("headings.toc")) {
|
|
20
|
+
const defaults = (await siteHeadingsSettings()).toc;
|
|
21
|
+
const levels = parseLevels(node.levels);
|
|
22
|
+
const tree = buildTocTree(storedHeadings(node._headings), levels.length ? levels : defaults.levels);
|
|
23
|
+
if (countToc(tree) >= defaults.minHeadings) {
|
|
24
|
+
const listStyle = ["none", "bulleted", "numbered"].includes(node.listStyle as string) ? (node.listStyle as TocListStyle) : defaults.listStyle;
|
|
25
|
+
const display = ["open", "collapsible", "collapsed"].includes(node.display as string) ? (node.display as TocDisplay) : defaults.display;
|
|
26
|
+
html = renderTocHtml(tree, {
|
|
27
|
+
title: (typeof node.title === "string" && node.title.trim()) || defaults.title,
|
|
28
|
+
showTitle: node.showTitle === "show" ? true : node.showTitle === "hide" ? false : defaults.showTitle,
|
|
29
|
+
titleTag: tocTitleTag(node.titleTag),
|
|
30
|
+
listStyle,
|
|
31
|
+
display,
|
|
32
|
+
smooth: defaults.smoothScroll,
|
|
33
|
+
titleId,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The CSS goes out once per page, before the first table.
|
|
39
|
+
const locals = (Astro.locals ?? {}) as Record<string, unknown>;
|
|
40
|
+
const css = html && !locals.__cwTocCss ? TOC_CSS : "";
|
|
41
|
+
if (css) locals.__cwTocCss = true;
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
{css && <style is:inline set:html={css} />}
|
|
45
|
+
{html && <Fragment set:html={html} />}
|