@cardinali-studio/adminkit 1.0.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/dist/api.d.ts +195 -0
- package/dist/api.js +118 -0
- package/dist/app/App.d.ts +3 -0
- package/dist/app/App.js +320 -0
- package/dist/app/app.css +2068 -0
- package/dist/app/index.d.ts +18 -0
- package/dist/app/index.js +33 -0
- package/dist/app/lib/bookings.d.ts +132 -0
- package/dist/app/lib/bookings.js +79 -0
- package/dist/app/lib/deliveries.d.ts +95 -0
- package/dist/app/lib/deliveries.js +104 -0
- package/dist/app/lib/delivery-drop.d.ts +1 -0
- package/dist/app/lib/delivery-drop.js +72 -0
- package/dist/app/lib/delivery-upload.d.ts +78 -0
- package/dist/app/lib/delivery-upload.js +585 -0
- package/dist/app/lib/groups.d.ts +9 -0
- package/dist/app/lib/groups.js +32 -0
- package/dist/app/lib/home.d.ts +11 -0
- package/dist/app/lib/home.js +67 -0
- package/dist/app/lib/overview.d.ts +82 -0
- package/dist/app/lib/overview.js +68 -0
- package/dist/app/lib/pages.d.ts +24 -0
- package/dist/app/lib/pages.js +156 -0
- package/dist/app/lib/publish.d.ts +18 -0
- package/dist/app/lib/publish.js +63 -0
- package/dist/app/lib/reorder.d.ts +3 -0
- package/dist/app/lib/reorder.js +31 -0
- package/dist/app/lib/requests.d.ts +10 -0
- package/dist/app/lib/requests.js +67 -0
- package/dist/app/lib/router.d.ts +1 -0
- package/dist/app/lib/router.js +14 -0
- package/dist/app/lib/settings.d.ts +29 -0
- package/dist/app/lib/settings.js +82 -0
- package/dist/app/lib/slug.d.ts +3 -0
- package/dist/app/lib/slug.js +23 -0
- package/dist/app/lib/text.d.ts +1 -0
- package/dist/app/lib/text.js +13 -0
- package/dist/app/lib/theme.d.ts +7 -0
- package/dist/app/lib/theme.js +43 -0
- package/dist/app/lib/upload-queue.d.ts +35 -0
- package/dist/app/lib/upload-queue.js +167 -0
- package/dist/app/routes/AboutEditor.d.ts +15 -0
- package/dist/app/routes/AboutEditor.js +122 -0
- package/dist/app/routes/Bookings.d.ts +21 -0
- package/dist/app/routes/Bookings.js +134 -0
- package/dist/app/routes/Deliveries.d.ts +37 -0
- package/dist/app/routes/Deliveries.js +563 -0
- package/dist/app/routes/GallerySectionsEditor.d.ts +12 -0
- package/dist/app/routes/GallerySectionsEditor.js +107 -0
- package/dist/app/routes/HomeEditor.d.ts +10 -0
- package/dist/app/routes/HomeEditor.js +188 -0
- package/dist/app/routes/Overview.d.ts +17 -0
- package/dist/app/routes/Overview.js +318 -0
- package/dist/app/routes/PageActions.d.ts +12 -0
- package/dist/app/routes/PageActions.js +93 -0
- package/dist/app/routes/PageEditor.d.ts +23 -0
- package/dist/app/routes/PageEditor.js +233 -0
- package/dist/app/routes/PageSeoEditor.d.ts +13 -0
- package/dist/app/routes/PageSeoEditor.js +102 -0
- package/dist/app/routes/PagesList.d.ts +10 -0
- package/dist/app/routes/PagesList.js +76 -0
- package/dist/app/routes/PortfolioWorksEditor.d.ts +12 -0
- package/dist/app/routes/PortfolioWorksEditor.js +139 -0
- package/dist/app/routes/Requests.d.ts +13 -0
- package/dist/app/routes/Requests.js +30 -0
- package/dist/app/routes/SiteSettingsEditor.d.ts +21 -0
- package/dist/app/routes/SiteSettingsEditor.js +66 -0
- package/dist/app/routes/UploadDock.d.ts +1 -0
- package/dist/app/routes/UploadDock.js +66 -0
- package/dist/app/routes/WorkshopEditionsEditor.d.ts +12 -0
- package/dist/app/routes/WorkshopEditionsEditor.js +101 -0
- package/dist/app/site.d.ts +37 -0
- package/dist/app/site.js +18 -0
- package/dist/app/ui/MediaSlot.d.ts +10 -0
- package/dist/app/ui/MediaSlot.js +20 -0
- package/dist/app/ui/OpenCard.d.ts +18 -0
- package/dist/app/ui/OpenCard.js +20 -0
- package/dist/app/ui/ParagraphEditor.d.ts +8 -0
- package/dist/app/ui/ParagraphEditor.js +37 -0
- package/dist/app/ui/dnd.d.ts +1 -0
- package/dist/app/ui/dnd.js +11 -0
- package/dist/app/ui/icons.d.ts +26 -0
- package/dist/app/ui/icons.js +45 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +26 -0
- package/dist/form/MediaBrowser.d.ts +13 -0
- package/dist/form/MediaBrowser.js +24 -0
- package/dist/form/MediaPicker.d.ts +10 -0
- package/dist/form/MediaPicker.js +20 -0
- package/dist/form/SchemaForm.d.ts +21 -0
- package/dist/form/SchemaForm.js +67 -0
- package/dist/form/Trash.d.ts +1 -0
- package/dist/form/Trash.js +42 -0
- package/dist/form/fields/index.d.ts +1 -0
- package/dist/form/fields/index.js +110 -0
- package/dist/form/fields/objectList.d.ts +1 -0
- package/dist/form/fields/objectList.js +99 -0
- package/dist/form/index.d.ts +8 -0
- package/dist/form/index.js +8 -0
- package/dist/form/media/Lightbox.d.ts +4 -0
- package/dist/form/media/Lightbox.js +70 -0
- package/dist/form/media/MediaFolders.d.ts +20 -0
- package/dist/form/media/MediaFolders.js +116 -0
- package/dist/form/media/MediaGrid.d.ts +8 -0
- package/dist/form/media/MediaGrid.js +139 -0
- package/dist/form/media/UploadDialog.d.ts +4 -0
- package/dist/form/media/UploadDialog.js +37 -0
- package/dist/form/media/useMediaLibrary.d.ts +55 -0
- package/dist/form/media/useMediaLibrary.js +495 -0
- package/dist/form/prose/ProseBar.d.ts +21 -0
- package/dist/form/prose/ProseBar.js +111 -0
- package/dist/form/prose/ProseField.d.ts +11 -0
- package/dist/form/prose/ProseField.js +152 -0
- package/dist/form/prose/pages.d.ts +6 -0
- package/dist/form/prose/pages.js +13 -0
- package/dist/form/registry.d.ts +20 -0
- package/dist/form/registry.js +15 -0
- package/dist/frontmatter.d.ts +20 -0
- package/dist/frontmatter.js +202 -0
- package/dist/functions/_middleware.js +101 -0
- package/dist/functions/_shared/analytics-ga4.js +248 -0
- package/dist/functions/_shared/analytics-umami.js +119 -0
- package/dist/functions/_shared/assets.js +90 -0
- package/dist/functions/_shared/b2.js +300 -0
- package/dist/functions/_shared/delivery-crypto.js +99 -0
- package/dist/functions/_shared/delivery-purge.js +222 -0
- package/dist/functions/_shared/edge-cache.js +37 -0
- package/dist/functions/_shared/exif.js +53 -0
- package/dist/functions/_shared/github.js +138 -0
- package/dist/functions/_shared/imagesize.js +101 -0
- package/dist/functions/_shared/media-content.js +200 -0
- package/dist/functions/_shared/pending-media.js +98 -0
- package/dist/functions/_shared/refs.js +155 -0
- package/dist/functions/api/analytics.js +106 -0
- package/dist/functions/api/assets/delete.js +241 -0
- package/dist/functions/api/assets/folder.js +43 -0
- package/dist/functions/api/assets/folders.js +32 -0
- package/dist/functions/api/assets/move.js +253 -0
- package/dist/functions/api/assets/preview.js +17 -0
- package/dist/functions/api/assets/trash/delete.js +13 -0
- package/dist/functions/api/assets/trash/empty.js +16 -0
- package/dist/functions/api/assets/trash/restore.js +43 -0
- package/dist/functions/api/assets/trash.js +28 -0
- package/dist/functions/api/assets/upload.js +54 -0
- package/dist/functions/api/assets/usage.js +41 -0
- package/dist/functions/api/assets.js +94 -0
- package/dist/functions/api/deliveries.js +629 -0
- package/dist/functions/api/delivery/hero.js +21 -0
- package/dist/functions/api/delivery/image.js +35 -0
- package/dist/functions/api/delivery/multipart.js +261 -0
- package/dist/functions/api/pages/get.js +26 -0
- package/dist/functions/api/pages.js +354 -0
- package/dist/functions/api/publish.js +134 -0
- package/dist/functions/api/requests.js +57 -0
- package/dist/functions/api/settings.js +236 -0
- package/dist/functions/api/singleton.js +105 -0
- package/dist/functions/api/storage.js +125 -0
- package/dist/functions/index.js +124 -0
- package/dist/functions/site.js +38 -0
- package/dist/gallery.d.ts +28 -0
- package/dist/gallery.js +18 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +47 -0
- package/dist/normalize.d.ts +50 -0
- package/dist/normalize.js +120 -0
- package/dist/preview.d.ts +3 -0
- package/dist/preview.js +82 -0
- package/dist/prose.d.ts +17 -0
- package/dist/prose.js +205 -0
- package/dist/registries.d.ts +43 -0
- package/dist/registries.js +29 -0
- package/dist/serialize.d.ts +26 -0
- package/dist/serialize.js +136 -0
- package/dist/testing.d.ts +26 -0
- package/dist/testing.js +66 -0
- package/dist/theme.css +636 -0
- package/dist/yaml.d.ts +8 -0
- package/dist/yaml.js +90 -0
- package/package.json +97 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* LO SMISTATORE: l'ingresso `./functions` del kit.
|
|
2
|
+
*
|
|
3
|
+
* Cloudflare Pages instrada le Functions PER FILE, e un pacchetto npm non può mettere file
|
|
4
|
+
* dentro `functions/`. Quindi il sito ne tiene due, entrambi da una riga — un acchiappatutto su
|
|
5
|
+
* `/admin/api/*` e il controllo d'accesso — e tutto il resto vive qui.
|
|
6
|
+
*
|
|
7
|
+
* functions/admin/api/[[path]].js -> handleAdminApi(context, config)
|
|
8
|
+
* functions/admin/_middleware.js -> adminAccess
|
|
9
|
+
*
|
|
10
|
+
* L'acchiappatutto è ristretto a `…/api/*` di proposito: uno un livello più in alto passerebbe
|
|
11
|
+
* per lo smistatore anche i file statici del pannello. Restano invece sotto il middleware — che
|
|
12
|
+
* è giusto, perché l'HTML e il JS del pannello devono stare dietro Access esattamente come le
|
|
13
|
+
* sue API (verificato: senza le variabili di Access, anche `/admin/index.html` risponde 503).
|
|
14
|
+
*
|
|
15
|
+
* COSA QUESTO SOSTITUISCE, e cosa no. Rifà la corrispondenza fra percorso e gestore, che è
|
|
16
|
+
* l'unica cosa che serviva. NON rifà i segmenti variabili (`[id].js`): oggi nessuna rotta ne ha
|
|
17
|
+
* — l'unica che li usava era Prenotazioni, che in v1.0 resta al sito. Aggiungerli adesso
|
|
18
|
+
* sarebbe macchinario per un caso che non esiste; si aggiungono quando arriva.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ Un sito può tenersi endpoint PROPRI accanto a questi: Cloudflare instrada prima le rotte
|
|
21
|
+
* più specifiche, quindi un `functions/admin/api/booking/index.js` del sito vince
|
|
22
|
+
* sull'acchiappatutto (verificato, non dedotto). È il motivo per cui Prenotazioni può restare
|
|
23
|
+
* dov'è senza inventare nessun punto di innesto.
|
|
24
|
+
*/
|
|
25
|
+
import { setServerConfig, moduleEnabled } from "./site.js";
|
|
26
|
+
|
|
27
|
+
import * as analytics from "./api/analytics.js";
|
|
28
|
+
import * as assets from "./api/assets.js";
|
|
29
|
+
import * as assetsDelete from "./api/assets/delete.js";
|
|
30
|
+
import * as assetsFolder from "./api/assets/folder.js";
|
|
31
|
+
import * as assetsFolders from "./api/assets/folders.js";
|
|
32
|
+
import * as assetsMove from "./api/assets/move.js";
|
|
33
|
+
import * as assetsPreview from "./api/assets/preview.js";
|
|
34
|
+
import * as assetsTrash from "./api/assets/trash.js";
|
|
35
|
+
import * as assetsTrashDelete from "./api/assets/trash/delete.js";
|
|
36
|
+
import * as assetsTrashEmpty from "./api/assets/trash/empty.js";
|
|
37
|
+
import * as assetsTrashRestore from "./api/assets/trash/restore.js";
|
|
38
|
+
import * as assetsUpload from "./api/assets/upload.js";
|
|
39
|
+
import * as assetsUsage from "./api/assets/usage.js";
|
|
40
|
+
import * as deliveries from "./api/deliveries.js";
|
|
41
|
+
import * as deliveryHero from "./api/delivery/hero.js";
|
|
42
|
+
import * as deliveryImage from "./api/delivery/image.js";
|
|
43
|
+
import * as deliveryMultipart from "./api/delivery/multipart.js";
|
|
44
|
+
import * as pages from "./api/pages.js";
|
|
45
|
+
import * as pagesGet from "./api/pages/get.js";
|
|
46
|
+
import * as publish from "./api/publish.js";
|
|
47
|
+
import * as requests from "./api/requests.js";
|
|
48
|
+
import * as settings from "./api/settings.js";
|
|
49
|
+
import * as singleton from "./api/singleton.js";
|
|
50
|
+
import * as storage from "./api/storage.js";
|
|
51
|
+
|
|
52
|
+
export { adminAccess } from "./_middleware.js";
|
|
53
|
+
export { setServerConfig } from "./site.js";
|
|
54
|
+
|
|
55
|
+
/* La tabella. `module` dice a quale interruttore appartiene una rotta: `null` = sempre presente.
|
|
56
|
+
*
|
|
57
|
+
* Oggi l'unico modulo che spegne le proprie rotte è Consegne, ed è deliberato: spegnerne altre
|
|
58
|
+
* cambierebbe come si comporta un pannello già in uso, e la v1.0 ha una lista ENUMERATA di
|
|
59
|
+
* cambiamenti di comportamento a cui attenersi. Gli altri interruttori arrivano quando c'è modo
|
|
60
|
+
* di verificarli uno per uno. */
|
|
61
|
+
const ROUTES = {
|
|
62
|
+
"analytics": { mod: null, h: analytics },
|
|
63
|
+
"assets": { mod: null, h: assets },
|
|
64
|
+
"assets/delete": { mod: null, h: assetsDelete },
|
|
65
|
+
"assets/folder": { mod: null, h: assetsFolder },
|
|
66
|
+
"assets/folders": { mod: null, h: assetsFolders },
|
|
67
|
+
"assets/move": { mod: null, h: assetsMove },
|
|
68
|
+
"assets/preview": { mod: null, h: assetsPreview },
|
|
69
|
+
"assets/trash": { mod: null, h: assetsTrash },
|
|
70
|
+
"assets/trash/delete": { mod: null, h: assetsTrashDelete },
|
|
71
|
+
"assets/trash/empty": { mod: null, h: assetsTrashEmpty },
|
|
72
|
+
"assets/trash/restore": { mod: null, h: assetsTrashRestore },
|
|
73
|
+
"assets/upload": { mod: null, h: assetsUpload },
|
|
74
|
+
"assets/usage": { mod: null, h: assetsUsage },
|
|
75
|
+
"deliveries": { mod: "delivery", h: deliveries },
|
|
76
|
+
"delivery/hero": { mod: "delivery", h: deliveryHero },
|
|
77
|
+
"delivery/image": { mod: "delivery", h: deliveryImage },
|
|
78
|
+
"delivery/multipart": { mod: "delivery", h: deliveryMultipart },
|
|
79
|
+
"pages": { mod: null, h: pages },
|
|
80
|
+
"pages/get": { mod: null, h: pagesGet },
|
|
81
|
+
"publish": { mod: null, h: publish },
|
|
82
|
+
"requests": { mod: null, h: requests },
|
|
83
|
+
"settings": { mod: null, h: settings },
|
|
84
|
+
"singleton": { mod: null, h: singleton },
|
|
85
|
+
"storage": { mod: null, h: storage },
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const PREFIX = "/admin/api/";
|
|
89
|
+
|
|
90
|
+
function json(data, status) {
|
|
91
|
+
return new Response(JSON.stringify(data), {
|
|
92
|
+
status,
|
|
93
|
+
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function handleAdminApi(context, config) {
|
|
98
|
+
setServerConfig(config);
|
|
99
|
+
|
|
100
|
+
const url = new URL(context.request.url);
|
|
101
|
+
if (!url.pathname.startsWith(PREFIX)) return json({ ok: false, error: "rotta fuori da /admin/api/" }, 404);
|
|
102
|
+
|
|
103
|
+
/* Le barre di troppo e finali non fanno una rotta diversa: `/assets/` e `/assets` sono la
|
|
104
|
+
* stessa cosa per chi la scrive, e farle divergere produce un 404 che sembra un guasto. */
|
|
105
|
+
const path = url.pathname.slice(PREFIX.length).split("/").filter(Boolean).join("/");
|
|
106
|
+
const route = ROUTES[path];
|
|
107
|
+
if (!route) return json({ ok: false, error: "rotta sconosciuta" }, 404);
|
|
108
|
+
|
|
109
|
+
/* Un modulo spento risponde 404, non 403: dall'esterno quella funzione non esiste, e un 403
|
|
110
|
+
* racconterebbe che esiste ma è vietata. */
|
|
111
|
+
if (route.mod && !moduleEnabled(route.mod)) {
|
|
112
|
+
return json({ ok: false, error: `Funzione "${route.mod}" non attiva.`, code: "feature_inactive" }, 404);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const method = context.request.method.toUpperCase();
|
|
116
|
+
const handler = route.h[`onRequest${method.charAt(0)}${method.slice(1).toLowerCase()}`] ?? route.h.onRequest;
|
|
117
|
+
if (!handler) {
|
|
118
|
+
/* 405 e non 404: la rotta c'è, è il verbo a non esserci. La differenza conta quando si
|
|
119
|
+
* cerca il guasto — «non esiste» e «non così» portano a cercare in due posti diversi. */
|
|
120
|
+
return json({ ok: false, error: "metodo non ammesso" }, 405);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return handler({ ...context, config });
|
|
124
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* LA GIUNTURA lato Worker: ciò che gli endpoint non possono sapere da soli.
|
|
2
|
+
*
|
|
3
|
+
* Gemella di `src/app/site.ts`, che fa la stessa cosa per l'interfaccia. Sono due perché sono
|
|
4
|
+
* due mondi diversi — uno gira nel browser e conosce React, l'altro gira in un Worker e non
|
|
5
|
+
* deve nemmeno vederlo — ma il sito le riempie **dalla stessa dichiarazione**: `site.config`
|
|
6
|
+
* è dati puri, quindi lo importano entrambi senza che React finisca dentro il Worker.
|
|
7
|
+
*
|
|
8
|
+
* Prima queste cose arrivavano con un `import` diretto dallo schema del cliente (e, in un caso,
|
|
9
|
+
* con un `import` del suo `data/site.json` — cioè un percorso che usciva dal pacchetto, che in
|
|
10
|
+
* un pacchetto pubblicato semplicemente non esiste).
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ Provvisoria quanto la sua gemella: in v1.1, con gli schemi per tipo, `page` diventa una
|
|
13
|
+
* mappa e metà di questi campi sparisce. Vedi l'intestazione di `src/app/site.ts`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
let current = null;
|
|
17
|
+
|
|
18
|
+
/** Chiamata dallo smistatore a ogni richiesta: il valore è costante per deploy, riscriverlo
|
|
19
|
+
* è idempotente e costa niente. */
|
|
20
|
+
export function setServerConfig(config) {
|
|
21
|
+
current = config;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function site() {
|
|
25
|
+
if (!current) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"adminkit: configurazione del sito non impostata. " +
|
|
28
|
+
"Gli endpoint si servono passando da `handleAdminApi(context, config)`.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Un modulo è acceso? Ciò che non è dichiarato è SPENTO: un interruttore dimenticato deve
|
|
35
|
+
* produrre una funzione assente, non una accesa per sbaglio. */
|
|
36
|
+
export function moduleEnabled(name) {
|
|
37
|
+
return current?.modules?.[name] === true;
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type GalleryImage = {
|
|
2
|
+
src: string;
|
|
3
|
+
w?: number | null;
|
|
4
|
+
h?: number | null;
|
|
5
|
+
hidden?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type SectionType = "header" | "text" | "image" | "gallery";
|
|
8
|
+
/** Una sezione della pagina gallery. Ha TUTTI i campi qualunque sia il tipo: Zod e React vogliono
|
|
9
|
+
* una forma stabile, e l'encoder emette poi solo quelli pertinenti (vedi `sections` più sotto). */
|
|
10
|
+
export interface PageSection {
|
|
11
|
+
id: string;
|
|
12
|
+
type: SectionType;
|
|
13
|
+
eyebrow: string;
|
|
14
|
+
heading: string;
|
|
15
|
+
lead: string;
|
|
16
|
+
paragraphs: string[];
|
|
17
|
+
image: string;
|
|
18
|
+
w: number | null;
|
|
19
|
+
h: number | null;
|
|
20
|
+
alt: string;
|
|
21
|
+
caption: string;
|
|
22
|
+
layout: "wide" | "contained";
|
|
23
|
+
images: GalleryImage[];
|
|
24
|
+
}
|
|
25
|
+
export declare const GALLERY_BLOCK_ID = "gallery";
|
|
26
|
+
/** Una sezione ai valori di riposo. Un posto solo in cui aggiungere un campo, invece di due
|
|
27
|
+
* (qui e nell'admin) che nessuno modifica insieme. */
|
|
28
|
+
export declare function emptySection(type: SectionType, id: string): PageSection;
|
package/dist/gallery.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* L'id del blocco gallery quando non ce l'ha ancora: lo usano la migrazione qui sotto e l'admin,
|
|
2
|
+
* che il blocco lo mostra sempre anche quando il front matter non lo contiene.
|
|
3
|
+
* È COSTANTE, e non generato come gli altri (`Date.now()` + random): un id nuovo a ogni render
|
|
4
|
+
* darebbe a dnd-kit una chiave nuova ogni volta, la card verrebbe rimontata di continuo e
|
|
5
|
+
* trascinarla diventerebbe impossibile. */
|
|
6
|
+
export const GALLERY_BLOCK_ID = "gallery";
|
|
7
|
+
/** Una sezione ai valori di riposo. Un posto solo in cui aggiungere un campo, invece di due
|
|
8
|
+
* (qui e nell'admin) che nessuno modifica insieme. */
|
|
9
|
+
export function emptySection(type, id) {
|
|
10
|
+
return {
|
|
11
|
+
id, type,
|
|
12
|
+
eyebrow: "", heading: "", lead: "",
|
|
13
|
+
paragraphs: [],
|
|
14
|
+
image: "", w: null, h: null, alt: "", caption: "",
|
|
15
|
+
layout: "wide",
|
|
16
|
+
images: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./config.js";
|
|
2
|
+
export * from "./registries.js";
|
|
3
|
+
export * from "./frontmatter.js";
|
|
4
|
+
export * from "./yaml.js";
|
|
5
|
+
export * from "./serialize.js";
|
|
6
|
+
export * from "./model.js";
|
|
7
|
+
export * from "./api.js";
|
|
8
|
+
export * from "./prose.js";
|
|
9
|
+
export * from "./gallery.js";
|
|
10
|
+
export * from "./normalize.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./config.js";
|
|
2
|
+
export * from "./registries.js";
|
|
3
|
+
export * from "./frontmatter.js";
|
|
4
|
+
export * from "./yaml.js";
|
|
5
|
+
export * from "./serialize.js";
|
|
6
|
+
export * from "./model.js";
|
|
7
|
+
export * from "./api.js";
|
|
8
|
+
export * from "./prose.js";
|
|
9
|
+
export * from "./gallery.js";
|
|
10
|
+
export * from "./normalize.js";
|
package/dist/model.d.ts
ADDED
package/dist/model.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* Costruisce il modello di un elemento dal front matter GREZZO, senza passare dal markdown.
|
|
2
|
+
* È la via che serve al manifest: Hugo emette i parametri così come sono nel file, e la
|
|
3
|
+
* conversione — il flip visible/published, la presenza del blocco `menu` — la fa lo schema.
|
|
4
|
+
* È l'unico modo per avere quella conversione in un posto solo: i template Hugo non possono
|
|
5
|
+
* importare TypeScript, quindi devono smettere di conoscerla del tutto. */
|
|
6
|
+
import { fm } from "./registries.js";
|
|
7
|
+
export function modelFromRaw(raw, shape, extra = {}) {
|
|
8
|
+
const normalized = withSchemaCasing(raw, shape);
|
|
9
|
+
const model = { ...extra };
|
|
10
|
+
for (const [name, field] of Object.entries(shape)) {
|
|
11
|
+
const meta = fm.get(field);
|
|
12
|
+
if (!meta)
|
|
13
|
+
continue;
|
|
14
|
+
const v = meta.decode(normalized, { raw: normalized });
|
|
15
|
+
if (v !== undefined)
|
|
16
|
+
model[name] = v;
|
|
17
|
+
}
|
|
18
|
+
return model;
|
|
19
|
+
}
|
|
20
|
+
/* HUGO MINUSCOLIZZA LE CHIAVI DEI PARAMETRI. `r2Prefix` nel file diventa `r2prefix` in
|
|
21
|
+
* `.Params`, `isHome` diventa `ishome`. Le `decode` sono scritte con la grafia del FILE — che è
|
|
22
|
+
* quella giusta, ed è quella che vede il serializzatore — quindi leggendo dal manifest non
|
|
23
|
+
* troverebbero niente e restituirebbero un valore vuoto.
|
|
24
|
+
*
|
|
25
|
+
* Non è teorico: `r2Prefix` spariva da tutte e quattro le gallery. E `isHome` sembrava
|
|
26
|
+
* funzionare solo perché vale `false` ovunque — con una pagina a `true` avrebbe sbagliato in
|
|
27
|
+
* silenzio, che è il modo peggiore.
|
|
28
|
+
*
|
|
29
|
+
* Qui si rimettono le chiavi nella grafia dichiarata dallo schema. Solo per quelle dichiarate:
|
|
30
|
+
* non è una normalizzazione generale, è una riconciliazione fra due sorgenti che chiamano la
|
|
31
|
+
* stessa cosa in due modi. */
|
|
32
|
+
function withSchemaCasing(raw, shape) {
|
|
33
|
+
const lower = new Map();
|
|
34
|
+
for (const [k, v] of Object.entries(raw))
|
|
35
|
+
lower.set(k.toLowerCase(), v);
|
|
36
|
+
const out = { ...raw };
|
|
37
|
+
for (const field of Object.values(shape)) {
|
|
38
|
+
for (const key of fm.get(field)?.keys ?? []) {
|
|
39
|
+
if (Object.prototype.hasOwnProperty.call(out, key))
|
|
40
|
+
continue;
|
|
41
|
+
const hit = lower.get(key.toLowerCase());
|
|
42
|
+
if (hit !== undefined)
|
|
43
|
+
out[key] = hit;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type Credit = {
|
|
2
|
+
label: string;
|
|
3
|
+
name: string;
|
|
4
|
+
href: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function normalizeCredits(value: unknown): Credit[];
|
|
7
|
+
export type WorkCardType = "image" | "text" | "frosted";
|
|
8
|
+
export type WorkInfo = {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
};
|
|
12
|
+
export type WorkCard = {
|
|
13
|
+
type: WorkCardType;
|
|
14
|
+
image: string;
|
|
15
|
+
alt: string;
|
|
16
|
+
kicker: string;
|
|
17
|
+
heading: string;
|
|
18
|
+
body: string[];
|
|
19
|
+
};
|
|
20
|
+
export type Work = {
|
|
21
|
+
title: string;
|
|
22
|
+
cover: string;
|
|
23
|
+
description: string[];
|
|
24
|
+
info: WorkInfo[];
|
|
25
|
+
cards: WorkCard[];
|
|
26
|
+
};
|
|
27
|
+
export declare const CARD_KINDS: {
|
|
28
|
+
type: WorkCardType;
|
|
29
|
+
label: string;
|
|
30
|
+
hint: string;
|
|
31
|
+
}[];
|
|
32
|
+
export declare function emptyWork(): Work;
|
|
33
|
+
export declare function emptyCard(type: WorkCardType): WorkCard;
|
|
34
|
+
export declare function normalizeWorks(value: unknown): Work[];
|
|
35
|
+
export type WorkshopBlock = {
|
|
36
|
+
heading: string;
|
|
37
|
+
body: string[];
|
|
38
|
+
};
|
|
39
|
+
export type Workshop = {
|
|
40
|
+
image: string;
|
|
41
|
+
kicker: string;
|
|
42
|
+
title: string;
|
|
43
|
+
lead: string;
|
|
44
|
+
blocks: WorkshopBlock[];
|
|
45
|
+
};
|
|
46
|
+
export declare function emptyWorkshop(): Workshop;
|
|
47
|
+
export declare function normalizeWorkshops(value: unknown): Workshop[];
|
|
48
|
+
import { type PageSection } from "./gallery.js";
|
|
49
|
+
export declare function normalizeSections(value: unknown): PageSection[];
|
|
50
|
+
export declare function withGalleryBlock(sections: PageSection[]): PageSection[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* I NORMALIZZATORI degli editor per tipo di pagina, e i tipi che portano con sé.
|
|
2
|
+
*
|
|
3
|
+
* Stavano dentro i rispettivi componenti React. Sono qui per due ragioni, e la seconda è quella
|
|
4
|
+
* che conta:
|
|
5
|
+
*
|
|
6
|
+
* 1. Sono logica pura — nessun JSX, nessuno stato, nessun hook — e stare dentro un file
|
|
7
|
+
* `.tsx` non le rendeva tali, le nascondeva soltanto.
|
|
8
|
+
*
|
|
9
|
+
* 2. SERVONO FUORI DALL'INTERFACCIA. Ogni sito deve poter verificare che normalizzare i
|
|
10
|
+
* PROPRI contenuti veri non li cambi: l'editor rimanda indietro la lista normalizzata, non
|
|
11
|
+
* quella che ha ricevuto, quindi se normalizzare alterasse anche un carattere, la prima
|
|
12
|
+
* modifica a un campo qualunque riscriverebbe in silenzio tutte le altre voci — e nel diff
|
|
13
|
+
* comparirebbero righe che nessuno ha toccato. Quella verifica gira in Node, dove React non
|
|
14
|
+
* c'è: importarla da un `.tsx` trascinerebbe l'intera interfaccia dentro una suite di test.
|
|
15
|
+
*
|
|
16
|
+
* Il modo in cui quella garanzia veniva ottenuta prima è istruttivo: il test del sito APRIVA il
|
|
17
|
+
* file `.tsx` dell'editor e ne ritagliava la funzione col testo, «perché una copia è un posto in
|
|
18
|
+
* più in cui divergere». Aveva ragione sul problema e non aveva alternative sulla soluzione.
|
|
19
|
+
* Con un pacchetto l'alternativa c'è, ed è questa.
|
|
20
|
+
*/
|
|
21
|
+
const str = (value) => (typeof value === "string" ? value : "");
|
|
22
|
+
const list = (value) => (Array.isArray(value) ? value : []);
|
|
23
|
+
const obj = (value) => (value ?? {});
|
|
24
|
+
export function normalizeCredits(value) {
|
|
25
|
+
return list(value).map((raw) => {
|
|
26
|
+
const item = obj(raw);
|
|
27
|
+
return { label: str(item.label), name: str(item.name), href: str(item.href) };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export const CARD_KINDS = [
|
|
31
|
+
{ type: "image", label: "Immagine", hint: "Una fotografia a tutta larghezza" },
|
|
32
|
+
{ type: "text", label: "Testo", hint: "Titolo e paragrafi" },
|
|
33
|
+
{ type: "frosted", label: "Testo su immagine", hint: "Parole sopra una fotografia" },
|
|
34
|
+
];
|
|
35
|
+
export function emptyWork() {
|
|
36
|
+
return { title: "", cover: "", description: [], info: [], cards: [] };
|
|
37
|
+
}
|
|
38
|
+
/* Il tipo di una card nuova è SEMPRE esplicito, mai stringa vuota: un `select` mostra comunque
|
|
39
|
+
* la prima opzione quando il valore non combacia con nessuna, quindi una card salvata senza
|
|
40
|
+
* toccare il campo finirebbe nel file con `type: ""` — un valore che non corrisponde a nessun
|
|
41
|
+
* ramo del template, cioè una card vuota sul sito. È già successo. */
|
|
42
|
+
export function emptyCard(type) {
|
|
43
|
+
return { type, image: "", alt: "", kicker: "", heading: "", body: [] };
|
|
44
|
+
}
|
|
45
|
+
export function normalizeWorks(value) {
|
|
46
|
+
return list(value).map((raw) => {
|
|
47
|
+
const item = obj(raw);
|
|
48
|
+
return {
|
|
49
|
+
title: str(item.title),
|
|
50
|
+
cover: str(item.cover),
|
|
51
|
+
description: list(item.description).map(str).filter(Boolean),
|
|
52
|
+
info: list(item.info).map((entry) => {
|
|
53
|
+
const o = obj(entry);
|
|
54
|
+
return { label: str(o.label), value: str(o.value) };
|
|
55
|
+
}),
|
|
56
|
+
cards: list(item.cards).map((entry) => {
|
|
57
|
+
const o = obj(entry);
|
|
58
|
+
const type = String(o.type);
|
|
59
|
+
return {
|
|
60
|
+
...emptyCard(CARD_KINDS.some((k) => k.type === type) ? type : "image"),
|
|
61
|
+
image: str(o.image),
|
|
62
|
+
alt: str(o.alt),
|
|
63
|
+
kicker: str(o.kicker),
|
|
64
|
+
heading: str(o.heading),
|
|
65
|
+
body: list(o.body).map(str),
|
|
66
|
+
};
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function emptyWorkshop() {
|
|
72
|
+
return { image: "", kicker: "", title: "", lead: "", blocks: [] };
|
|
73
|
+
}
|
|
74
|
+
export function normalizeWorkshops(value) {
|
|
75
|
+
return list(value).map((raw) => {
|
|
76
|
+
const item = obj(raw);
|
|
77
|
+
return {
|
|
78
|
+
image: str(item.image),
|
|
79
|
+
kicker: str(item.kicker),
|
|
80
|
+
title: str(item.title),
|
|
81
|
+
lead: str(item.lead),
|
|
82
|
+
blocks: list(item.blocks).map((entry) => {
|
|
83
|
+
const block = obj(entry);
|
|
84
|
+
return { heading: str(block.heading), body: list(block.body).map(str) };
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/* ---- Gallery: le sezioni ------------------------------------------------------------------- */
|
|
90
|
+
import { emptySection, GALLERY_BLOCK_ID } from "./gallery.js";
|
|
91
|
+
export function normalizeSections(value) {
|
|
92
|
+
if (!Array.isArray(value))
|
|
93
|
+
return [];
|
|
94
|
+
return value
|
|
95
|
+
.map((raw, index) => {
|
|
96
|
+
const item = (raw ?? {});
|
|
97
|
+
if (!["header", "text", "image", "gallery"].includes(String(item.type)))
|
|
98
|
+
return null;
|
|
99
|
+
return {
|
|
100
|
+
...emptySection(item.type, String(item.id || `section-${index + 1}`)),
|
|
101
|
+
...item,
|
|
102
|
+
id: String(item.id || `section-${index + 1}`),
|
|
103
|
+
paragraphs: Array.isArray(item.paragraphs) ? item.paragraphs.map(String) : [],
|
|
104
|
+
w: Number(item.w) > 0 ? Number(item.w) : null,
|
|
105
|
+
h: Number(item.h) > 0 ? Number(item.h) : null,
|
|
106
|
+
layout: item.layout === "contained" ? "contained" : "wide",
|
|
107
|
+
images: Array.isArray(item.images) ? item.images : [],
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
.filter((item) => !!item);
|
|
111
|
+
}
|
|
112
|
+
/* Il blocco delle foto c'è sempre. Se il front matter non lo contiene ancora — una gallery mai
|
|
113
|
+
* salvata dal pannello, o una pagina appena diventata gallery — lo si aggiunge in coda, che è
|
|
114
|
+
* dove la griglia usciva prima che questo blocco esistesse. Vive solo nella vista finché
|
|
115
|
+
* l'utente non tocca qualcosa: aprire una pagina non deve sporcarla. */
|
|
116
|
+
export function withGalleryBlock(sections) {
|
|
117
|
+
return sections.some((section) => section.type === "gallery")
|
|
118
|
+
? sections
|
|
119
|
+
: [...sections, emptySection("gallery", GALLERY_BLOCK_ID)];
|
|
120
|
+
}
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* Pannello "Anteprima": mostra dentro l'admin lo stato dell'anteprima locale gestita da Regia.
|
|
3
|
+
*
|
|
4
|
+
* Due cose da tenere a mente, perché sembrano mancanze e non lo sono:
|
|
5
|
+
*
|
|
6
|
+
* 1. Da qui si può solo FERMARE e RIAVVIARE. Non accendere: per vedere questa pagina il sito
|
|
7
|
+
* deve già girare, quindi l'accensione non può che vivere nel cruscotto di Regia. È un
|
|
8
|
+
* limite del mezzo, e va scritto nell'interfaccia o sembra una funzione dimenticata.
|
|
9
|
+
* 2. Regia è facoltativa. Se il demone non c'è il pannello lo dice e basta: niente errori
|
|
10
|
+
* rumorosi, niente riprove, e soprattutto nessuna dipendenza dell'admin da uno strumento
|
|
11
|
+
* che su un'altra macchina potrebbe non essere installato.
|
|
12
|
+
*/
|
|
13
|
+
import { useCallback, useEffect, useState } from "react";
|
|
14
|
+
const DAEMON_PORT = 7391;
|
|
15
|
+
const DAEMON = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
16
|
+
/** Vero solo su un'anteprima locale. In produzione la scheda non va nemmeno emessa. */
|
|
17
|
+
export function isLocalPreview() {
|
|
18
|
+
if (typeof location === "undefined")
|
|
19
|
+
return false;
|
|
20
|
+
return location.hostname === "localhost" || location.hostname === "127.0.0.1";
|
|
21
|
+
}
|
|
22
|
+
const STATE_TEXT = {
|
|
23
|
+
running: "attiva",
|
|
24
|
+
starting: "in avvio…",
|
|
25
|
+
stopped: "ferma",
|
|
26
|
+
crashed: "caduta",
|
|
27
|
+
failed: "avvio fallito",
|
|
28
|
+
};
|
|
29
|
+
export function Preview() {
|
|
30
|
+
const [status, setStatus] = useState({ kind: "loading" });
|
|
31
|
+
const [busy, setBusy] = useState(false);
|
|
32
|
+
const load = useCallback(async () => {
|
|
33
|
+
const port = location.port || (location.protocol === "https:" ? "443" : "80");
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${DAEMON}/api/whoami?port=${encodeURIComponent(port)}`);
|
|
36
|
+
if (res.status === 404)
|
|
37
|
+
return setStatus({ kind: "unmanaged" });
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
return setStatus({ kind: "absent" });
|
|
40
|
+
const data = (await res.json());
|
|
41
|
+
setStatus({ kind: "site", site: data.site, dashboard: data.dashboard });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Il demone non risponde: non è un errore dell'admin, è uno strumento assente.
|
|
45
|
+
setStatus({ kind: "absent" });
|
|
46
|
+
}
|
|
47
|
+
}, []);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
void load();
|
|
50
|
+
}, [load]);
|
|
51
|
+
const act = async (action) => {
|
|
52
|
+
if (status.kind !== "site")
|
|
53
|
+
return;
|
|
54
|
+
if (action === "stop" && !confirm("Fermare l'anteprima? Questa pagina smetterà di rispondere."))
|
|
55
|
+
return;
|
|
56
|
+
setBusy(true);
|
|
57
|
+
try {
|
|
58
|
+
await fetch(`${DAEMON}/api/sites/${encodeURIComponent(status.site.id)}/${action}`, { method: "POST" });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* se il riavvio uccide la connessione va bene così: si ricarica sotto */
|
|
62
|
+
}
|
|
63
|
+
setBusy(false);
|
|
64
|
+
if (action === "restart") {
|
|
65
|
+
// Dopo un riavvio il server torna su con qualche secondo di ritardo.
|
|
66
|
+
setTimeout(() => void load(), 2500);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
void load();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
if (status.kind === "loading")
|
|
73
|
+
return _jsx("p", { className: "ak-hint", children: "Controllo\u2026" });
|
|
74
|
+
if (status.kind === "absent") {
|
|
75
|
+
return (_jsx("div", { className: "rg", children: _jsxs("p", { className: "ak-hint", children: ["Regia non \u00E8 in esecuzione, quindi questa anteprima non \u00E8 pilotabile da qui. Apri l'icona ", _jsx("strong", { children: "Regia" }), ", oppure il cruscotto: ", _jsxs("a", { href: `${DAEMON}/`, children: [DAEMON, "/"] })] }) }));
|
|
76
|
+
}
|
|
77
|
+
if (status.kind === "unmanaged") {
|
|
78
|
+
return (_jsx("div", { className: "rg", children: _jsx("p", { className: "ak-hint", children: "Questa anteprima non \u00E8 gestita da Regia: \u00E8 stata avviata in un altro modo, quindi va fermata dove \u00E8 stata accesa." }) }));
|
|
79
|
+
}
|
|
80
|
+
const { site, dashboard } = status;
|
|
81
|
+
return (_jsxs("div", { className: "rg", children: [_jsxs("header", { className: "rg-top", children: [_jsx("span", { className: `rg-dot rg-dot-${site.state}`, "aria-hidden": "true" }), _jsx("span", { className: "rg-name", children: site.name }), _jsxs("span", { className: "ak-hint", children: ["anteprima ", STATE_TEXT[site.state], " \u00B7 porta ", site.port] }), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void act("restart"), children: "Riavvia" }), _jsx("button", { className: "ak-btn", disabled: busy, onClick: () => void act("stop"), children: "Ferma" })] }), site.message && _jsx("p", { className: "ak-hint", children: site.message }), _jsxs("p", { className: "ak-hint", children: ["Da qui si pu\u00F2 solo fermare o riavviare: per accendere un'anteprima serve il cruscotto, ", _jsx("a", { href: dashboard, children: dashboard })] }), site.lan && site.urls.lan.length > 0 ? (_jsxs("div", { className: "rg-lan", children: [_jsxs("div", { className: "rg-lan-urls", children: [_jsxs("p", { className: "ak-hint", children: ["Dal telefono, sulla stessa rete \u2014 si vede ", _jsx("strong", { children: "il sito" }), ": questo pannello no, perch\u00E9 la via d'uscita di Access vale solo su un hostname locale."] }), site.urls.lan.map((l) => (_jsxs("div", { children: [_jsx("a", { href: l.url, children: l.url }), l.kind === "mdns" && _jsx("span", { className: "ak-hint", children: " \u2014 se il nome non risponde, usa l'indirizzo numerico" })] }, l.url)))] }), _jsx("img", { className: "rg-qr", alt: "Codice QR dell'indirizzo di rete", src: `${DAEMON}/api/sites/${encodeURIComponent(site.id)}/qr.svg?url=${encodeURIComponent(site.urls.lan[0].url)}` })] })) : (_jsx("p", { className: "ak-hint", children: "Questa anteprima \u00E8 visibile solo da questo Mac. Per aprirla al telefono, accendi \u00ABSulla rete locale\u00BB nel cruscotto." }))] }));
|
|
82
|
+
}
|
package/dist/prose.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Un testo e la porzione selezionata dentro di lui. */
|
|
2
|
+
export interface Selection {
|
|
3
|
+
text: string;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
/** Il markdown come lo mostra il campo: un `<p>` per paragrafo, `<br>` per l'a capo singolo. */
|
|
8
|
+
export declare function mdToHtml(md: string): string;
|
|
9
|
+
/** Gli indirizzi che si possono scrivere in un collegamento.
|
|
10
|
+
*
|
|
11
|
+
* `javascript:` è la ragione per cui questo controllo esiste: il markdown dell'admin finisce in
|
|
12
|
+
* pagine renderizzate con `unsafe = true`, quindi un indirizzo non è un dato inerte. Si
|
|
13
|
+
* accettano gli schemi che servono e i percorsi interni; tutto il resto è rifiutato invece di
|
|
14
|
+
* essere ripulito, perché "ripulire" un indirizzo ostile è un gioco che si perde. */
|
|
15
|
+
export declare function isSafeHref(href: string): boolean;
|
|
16
|
+
/** Il testo del campo com'è da salvare: markdown. */
|
|
17
|
+
export declare function htmlToMd(html: string): string;
|