emdash 0.2.0 → 0.4.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/{adapters-N6BF7RCD.d.mts → adapters-C2BzVy0p.d.mts} +1 -1
- package/dist/{adapters-N6BF7RCD.d.mts.map → adapters-C2BzVy0p.d.mts.map} +1 -1
- package/dist/{apply-wmVEOSbR.mjs → apply-Cma_PiF6.mjs} +38 -23
- package/dist/apply-Cma_PiF6.mjs.map +1 -0
- package/dist/astro/index.d.mts +25 -11
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +38 -25
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +2 -2
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +20 -8
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +12 -2
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +52 -45
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +9 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-1WQPlISL.mjs → byline-WuOq9MFJ.mjs} +5 -4
- package/dist/byline-WuOq9MFJ.mjs.map +1 -0
- package/dist/{bylines-BYdTYmia.mjs → bylines-C_Wsnz4L.mjs} +38 -6
- package/dist/bylines-C_Wsnz4L.mjs.map +1 -0
- package/dist/cache-E3Dts-yT.mjs +56 -0
- package/dist/cache-E3Dts-yT.mjs.map +1 -0
- package/dist/cli/index.mjs +13 -13
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-Cq8H0SfX.mjs → config-DkxPrM9l.mjs} +1 -1
- package/dist/{config-Cq8H0SfX.mjs.map → config-DkxPrM9l.mjs.map} +1 -1
- package/dist/{content-BmXndhdi.mjs → content-BsBoyj8G.mjs} +20 -3
- package/dist/content-BsBoyj8G.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{default-WYlzADZL.mjs → default-PUx9RK6u.mjs} +1 -1
- package/dist/{default-WYlzADZL.mjs.map → default-PUx9RK6u.mjs.map} +1 -1
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +4 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +1 -0
- package/dist/{error-DrxtnGPg.mjs → error-HBeQbVhV.mjs} +1 -1
- package/dist/{error-DrxtnGPg.mjs.map → error-HBeQbVhV.mjs.map} +1 -1
- package/dist/{index-UHEVQMus.d.mts → index-CRg3PWfZ.d.mts} +59 -33
- package/dist/index-CRg3PWfZ.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +20 -20
- package/dist/{load-Veizk2cT.mjs → load-BhSSm-TS.mjs} +1 -1
- package/dist/{load-Veizk2cT.mjs.map → load-BhSSm-TS.mjs.map} +1 -1
- package/dist/{loader-CHb2v0jm.mjs → loader-BYzwzORf.mjs} +4 -2
- package/dist/loader-BYzwzORf.mjs.map +1 -0
- package/dist/{manifest-schema-CuMio1A9.mjs → manifest-schema-BsXINkQD.mjs} +1 -1
- package/dist/{manifest-schema-CuMio1A9.mjs.map → manifest-schema-BsXINkQD.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CYeM2rPt.mjs → mode-CyPLdO3C.mjs} +1 -1
- package/dist/{mode-CYeM2rPt.mjs.map → mode-CyPLdO3C.mjs.map} +1 -1
- package/dist/page/index.d.mts +1 -1
- package/dist/patterns-CrCYkMBb.mjs +93 -0
- package/dist/patterns-CrCYkMBb.mjs.map +1 -0
- package/dist/{placeholder-bOx1xCTY.d.mts → placeholder-BBCtpTES.d.mts} +1 -1
- package/dist/{placeholder-bOx1xCTY.d.mts.map → placeholder-BBCtpTES.d.mts.map} +1 -1
- package/dist/{placeholder-aiCD8aSZ.mjs → placeholder-DntBEQo7.mjs} +1 -1
- package/dist/{placeholder-aiCD8aSZ.mjs.map → placeholder-DntBEQo7.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-5Hcv_5ER.mjs → query-B6Vu0d2i.mjs} +35 -16
- package/dist/{query-5Hcv_5ER.mjs.map → query-B6Vu0d2i.mjs.map} +1 -1
- package/dist/{redirect-DIfIni3r.mjs → redirect-7lGhLBNZ.mjs} +10 -93
- package/dist/redirect-7lGhLBNZ.mjs.map +1 -0
- package/dist/{registry-1EvbAfsC.mjs → registry-BgnP3ysR.mjs} +27 -37
- package/dist/registry-BgnP3ysR.mjs.map +1 -0
- package/dist/{runner-BoN0-FPi.mjs → runner-Cd-_WyDo.mjs} +18 -6
- package/dist/runner-Cd-_WyDo.mjs.map +1 -0
- package/dist/{runner-DTqkzOzc.d.mts → runner-DYv3rX8P.d.mts} +10 -3
- package/dist/runner-DYv3rX8P.d.mts.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BsYMed12.mjs → search-B5p9D36n.mjs} +108 -57
- package/dist/search-B5p9D36n.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -10
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +11 -3
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +76 -15
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{tokens-DrB-W6Q-.mjs → tokens-DKHiCYCB.mjs} +1 -1
- package/dist/{tokens-DrB-W6Q-.mjs.map → tokens-DKHiCYCB.mjs.map} +1 -1
- package/dist/transaction-Cn2rjY78.mjs +28 -0
- package/dist/transaction-Cn2rjY78.mjs.map +1 -0
- package/dist/{transport-Bl8cTdYt.mjs → transport-BtcQ-Z7T.mjs} +1 -1
- package/dist/{transport-Bl8cTdYt.mjs.map → transport-BtcQ-Z7T.mjs.map} +1 -1
- package/dist/{transport-COOs9GSE.d.mts → transport-CKQA_G44.d.mts} +1 -1
- package/dist/{transport-COOs9GSE.d.mts.map → transport-CKQA_G44.d.mts.map} +1 -1
- package/dist/{types-7-UjSEyB.d.mts → types-B6BzlZxx.d.mts} +1 -1
- package/dist/{types-7-UjSEyB.d.mts.map → types-B6BzlZxx.d.mts.map} +1 -1
- package/dist/{types-6dqxBqsH.d.mts → types-BYWYxLcp.d.mts} +109 -5
- package/dist/types-BYWYxLcp.d.mts.map +1 -0
- package/dist/{types-CIsTnQvJ.d.mts → types-BmkQR1En.d.mts} +1 -1
- package/dist/{types-CIsTnQvJ.d.mts.map → types-BmkQR1En.d.mts.map} +1 -1
- package/dist/{types-BljtYPSd.d.mts → types-DNZpaCBk.d.mts} +14 -6
- package/dist/types-DNZpaCBk.d.mts.map +1 -0
- package/dist/{types-Bec-r_3_.mjs → types-Dz9_WMS6.mjs} +1 -1
- package/dist/types-Dz9_WMS6.mjs.map +1 -0
- package/dist/{types-CcreFIIH.d.mts → types-gLYVCXCQ.d.mts} +1 -1
- package/dist/{types-CcreFIIH.d.mts.map → types-gLYVCXCQ.d.mts.map} +1 -1
- package/dist/{types-DuNbGKjF.mjs → types-xxCWI3j0.mjs} +1 -1
- package/dist/{types-DuNbGKjF.mjs.map → types-xxCWI3j0.mjs.map} +1 -1
- package/dist/{validate-B7KP7VLM.d.mts → validate-CcNRWH6I.d.mts} +4 -4
- package/dist/{validate-B7KP7VLM.d.mts.map → validate-CcNRWH6I.d.mts.map} +1 -1
- package/dist/{validate-CXnRKfJK.mjs → validate-DuZDIxfy.mjs} +2 -2
- package/dist/{validate-CXnRKfJK.mjs.map → validate-DuZDIxfy.mjs.map} +1 -1
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +11 -11
- package/dist/{validate-CqRJb_xU.mjs.map → validate-VPnKoIzW.mjs.map} +1 -1
- package/dist/version-DlTDRdpv.mjs +7 -0
- package/dist/version-DlTDRdpv.mjs.map +1 -0
- package/package.json +7 -5
- package/src/api/handlers/content.ts +36 -25
- package/src/api/handlers/menus.ts +19 -16
- package/src/api/handlers/redirects.ts +95 -3
- package/src/api/schemas/redirects.ts +1 -0
- package/src/astro/integration/index.ts +2 -3
- package/src/astro/integration/runtime.ts +8 -14
- package/src/astro/integration/vite-config.ts +14 -4
- package/src/astro/middleware/redirect.ts +30 -15
- package/src/astro/middleware.ts +11 -19
- package/src/astro/routes/admin.astro +2 -2
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -0
- package/src/astro/routes/api/admin/bylines/index.ts +2 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +2 -0
- package/src/astro/routes/api/manifest.ts +3 -1
- package/src/astro/routes/api/redirects/[id].ts +3 -0
- package/src/astro/routes/api/redirects/index.ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -0
- package/src/astro/routes/api/schema/collections/index.ts +1 -0
- package/src/astro/storage/adapters.ts +19 -5
- package/src/astro/storage/types.ts +12 -4
- package/src/astro/types.ts +1 -0
- package/src/bylines/index.ts +50 -2
- package/src/cleanup.ts +3 -3
- package/src/cli/commands/bundle-utils.ts +5 -5
- package/src/database/dialect-helpers.ts +3 -0
- package/src/database/migrations/011_sections.ts +2 -2
- package/src/database/migrations/runner.ts +23 -2
- package/src/database/repositories/byline.ts +2 -1
- package/src/database/repositories/content.ts +5 -0
- package/src/database/repositories/redirect.ts +13 -0
- package/src/database/validate.ts +10 -10
- package/src/emdash-runtime.ts +23 -9
- package/src/index.ts +3 -0
- package/src/loader.ts +2 -0
- package/src/mcp/server.ts +40 -67
- package/src/menus/index.ts +4 -0
- package/src/plugins/context.ts +28 -4
- package/src/plugins/cron.ts +29 -4
- package/src/plugins/hooks.ts +22 -10
- package/src/plugins/index.ts +1 -0
- package/src/plugins/manager.ts +6 -2
- package/src/plugins/marketplace.ts +33 -3
- package/src/plugins/routes.ts +3 -3
- package/src/plugins/types.ts +7 -0
- package/src/query.ts +37 -14
- package/src/redirects/cache.ts +68 -0
- package/src/redirects/loops.ts +318 -0
- package/src/schema/registry.ts +3 -0
- package/src/search/fts-manager.ts +24 -11
- package/src/search/query.ts +8 -9
- package/src/seed/apply.ts +49 -28
- package/src/storage/s3.ts +94 -25
- package/src/storage/types.ts +13 -5
- package/src/utils/slugify.ts +11 -0
- package/src/version.ts +12 -0
- package/src/visual-editing/toolbar.ts +11 -1
- package/dist/apply-wmVEOSbR.mjs.map +0 -1
- package/dist/byline-1WQPlISL.mjs.map +0 -1
- package/dist/bylines-BYdTYmia.mjs.map +0 -1
- package/dist/content-BmXndhdi.mjs.map +0 -1
- package/dist/dialect-helpers-B9uSp2GJ.mjs.map +0 -1
- package/dist/index-UHEVQMus.d.mts.map +0 -1
- package/dist/loader-CHb2v0jm.mjs.map +0 -1
- package/dist/redirect-DIfIni3r.mjs.map +0 -1
- package/dist/registry-1EvbAfsC.mjs.map +0 -1
- package/dist/runner-BoN0-FPi.mjs.map +0 -1
- package/dist/runner-DTqkzOzc.d.mts.map +0 -1
- package/dist/search-BsYMed12.mjs.map +0 -1
- package/dist/types-6dqxBqsH.d.mts.map +0 -1
- package/dist/types-Bec-r_3_.mjs.map +0 -1
- package/dist/types-BljtYPSd.d.mts.map +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import "../../dialect-helpers-
|
|
1
|
+
import "../../dialect-helpers-DhTzaUxP.mjs";
|
|
2
2
|
import "../../base64-MBPo9ozB.mjs";
|
|
3
3
|
import "../../types-CMMN0pNg.mjs";
|
|
4
|
-
import { t as RedirectRepository } from "../../redirect-
|
|
4
|
+
import { t as RedirectRepository } from "../../redirect-7lGhLBNZ.mjs";
|
|
5
|
+
import { a as setCachedPatternRules, i as matchCachedPatterns, n as getCachedPatternRules } from "../../cache-E3Dts-yT.mjs";
|
|
5
6
|
import { defineMiddleware } from "astro:middleware";
|
|
6
7
|
|
|
7
8
|
//#region src/astro/middleware/redirect.ts
|
|
@@ -35,12 +36,23 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
35
36
|
if (!emdash?.db) return next();
|
|
36
37
|
try {
|
|
37
38
|
const repo = new RedirectRepository(emdash.db);
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
const exact = await repo.findExactMatch(pathname);
|
|
40
|
+
if (exact) {
|
|
41
|
+
const dest = exact.destination;
|
|
42
|
+
if (dest.startsWith("//") || dest.startsWith("/\\")) return next();
|
|
43
|
+
repo.recordHit(exact.id).catch(() => {});
|
|
44
|
+
const code = isRedirectCode(exact.type) ? exact.type : 301;
|
|
45
|
+
return context.redirect(dest, code);
|
|
46
|
+
}
|
|
47
|
+
let rules = getCachedPatternRules();
|
|
48
|
+
if (!rules) rules = setCachedPatternRules(await repo.findEnabledPatternRules());
|
|
49
|
+
const patternMatch = matchCachedPatterns(rules, pathname);
|
|
50
|
+
if (patternMatch) {
|
|
51
|
+
const { redirect, destination } = patternMatch;
|
|
52
|
+
if (destination.startsWith("//") || destination.startsWith("/\\")) return next();
|
|
53
|
+
repo.recordHit(redirect.id).catch(() => {});
|
|
54
|
+
const code = isRedirectCode(redirect.type) ? redirect.type : 301;
|
|
55
|
+
return context.redirect(destination, code);
|
|
44
56
|
}
|
|
45
57
|
const response = await next();
|
|
46
58
|
if (response.status === 404) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.mjs","names":[],"sources":["../../../src/astro/middleware/redirect.ts"],"sourcesContent":["/**\n * Redirect middleware\n *\n * Intercepts incoming requests and checks for matching redirect rules.\n * Runs after runtime init (needs db) but before setup/auth (should handle\n * ALL routes, including public ones, and should be fast).\n *\n * Skip paths:\n * - /_emdash/* (admin UI, API routes, auth endpoints)\n * - /_image (Astro image optimization)\n * - Static assets (files with extensions)\n *\n * 404 logging happens post-response: if next() returns 404 and the path\n * wasn't already matched by a redirect, log it.\n */\n\nimport { defineMiddleware } from \"astro:middleware\";\n\nimport { RedirectRepository } from \"../../database/repositories/redirect.js\";\n\n/** Paths that should never be intercepted by redirects */\nconst SKIP_PREFIXES = [\"/_emdash\", \"/_image\"];\n\n/** Static asset extensions -- don't redirect file requests */\nconst ASSET_EXTENSION = /\\.\\w{1,10}$/;\n\ntype RedirectCode = 301 | 302 | 303 | 307 | 308;\n\nfunction isRedirectCode(code: number): code is RedirectCode {\n\treturn code === 301 || code === 302 || code === 303 || code === 307 || code === 308;\n}\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { pathname } = context.url;\n\n\t// Skip internal paths and static assets\n\tif (SKIP_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {\n\t\treturn next();\n\t}\n\tif (ASSET_EXTENSION.test(pathname)) {\n\t\treturn next();\n\t}\n\n\tconst { emdash } = context.locals;\n\tif (!emdash?.db) {\n\t\treturn next();\n\t}\n\n\ttry {\n\t\tconst repo = new RedirectRepository(emdash.db);\n\t\
|
|
1
|
+
{"version":3,"file":"redirect.mjs","names":[],"sources":["../../../src/astro/middleware/redirect.ts"],"sourcesContent":["/**\n * Redirect middleware\n *\n * Intercepts incoming requests and checks for matching redirect rules.\n * Runs after runtime init (needs db) but before setup/auth (should handle\n * ALL routes, including public ones, and should be fast).\n *\n * Skip paths:\n * - /_emdash/* (admin UI, API routes, auth endpoints)\n * - /_image (Astro image optimization)\n * - Static assets (files with extensions)\n *\n * 404 logging happens post-response: if next() returns 404 and the path\n * wasn't already matched by a redirect, log it.\n */\n\nimport { defineMiddleware } from \"astro:middleware\";\n\nimport { RedirectRepository } from \"../../database/repositories/redirect.js\";\nimport {\n\tgetCachedPatternRules,\n\tmatchCachedPatterns,\n\tsetCachedPatternRules,\n} from \"../../redirects/cache.js\";\n\n/** Paths that should never be intercepted by redirects */\nconst SKIP_PREFIXES = [\"/_emdash\", \"/_image\"];\n\n/** Static asset extensions -- don't redirect file requests */\nconst ASSET_EXTENSION = /\\.\\w{1,10}$/;\n\ntype RedirectCode = 301 | 302 | 303 | 307 | 308;\n\nfunction isRedirectCode(code: number): code is RedirectCode {\n\treturn code === 301 || code === 302 || code === 303 || code === 307 || code === 308;\n}\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { pathname } = context.url;\n\n\t// Skip internal paths and static assets\n\tif (SKIP_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {\n\t\treturn next();\n\t}\n\tif (ASSET_EXTENSION.test(pathname)) {\n\t\treturn next();\n\t}\n\n\tconst { emdash } = context.locals;\n\tif (!emdash?.db) {\n\t\treturn next();\n\t}\n\n\ttry {\n\t\tconst repo = new RedirectRepository(emdash.db);\n\n\t\t// 1. Exact match (fast, indexed)\n\t\tconst exact = await repo.findExactMatch(pathname);\n\t\tif (exact) {\n\t\t\tconst dest = exact.destination;\n\t\t\tif (dest.startsWith(\"//\") || dest.startsWith(\"/\\\\\")) return next();\n\t\t\trepo.recordHit(exact.id).catch(() => {});\n\t\t\tconst code = isRedirectCode(exact.type) ? exact.type : 301;\n\t\t\treturn context.redirect(dest, code);\n\t\t}\n\n\t\t// 2. Pattern match (cached: compile once, match every request)\n\t\tlet rules = getCachedPatternRules();\n\t\tif (!rules) {\n\t\t\tconst patterns = await repo.findEnabledPatternRules();\n\t\t\trules = setCachedPatternRules(patterns);\n\t\t}\n\n\t\tconst patternMatch = matchCachedPatterns(rules, pathname);\n\t\tif (patternMatch) {\n\t\t\tconst { redirect, destination } = patternMatch;\n\t\t\tif (destination.startsWith(\"//\") || destination.startsWith(\"/\\\\\")) return next();\n\t\t\trepo.recordHit(redirect.id).catch(() => {});\n\t\t\tconst code = isRedirectCode(redirect.type) ? redirect.type : 301;\n\t\t\treturn context.redirect(destination, code);\n\t\t}\n\n\t\t// No redirect matched -- proceed and check for 404\n\t\tconst response = await next();\n\n\t\t// Log 404s for unmatched paths (fire-and-forget)\n\t\tif (response.status === 404) {\n\t\t\tconst referrer = context.request.headers.get(\"referer\") ?? null;\n\t\t\tconst userAgent = context.request.headers.get(\"user-agent\") ?? null;\n\t\t\trepo\n\t\t\t\t.log404({\n\t\t\t\t\tpath: pathname,\n\t\t\t\t\treferrer,\n\t\t\t\t\tuserAgent,\n\t\t\t\t})\n\t\t\t\t.catch(() => {});\n\t\t}\n\n\t\treturn response;\n\t} catch {\n\t\t// If the redirects table doesn't exist yet (pre-migration), skip silently\n\t\treturn next();\n\t}\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,gBAAgB,CAAC,YAAY,UAAU;;AAG7C,MAAM,kBAAkB;AAIxB,SAAS,eAAe,MAAoC;AAC3D,QAAO,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO,SAAS;;AAGjF,MAAa,YAAY,iBAAiB,OAAO,SAAS,SAAS;CAClE,MAAM,EAAE,aAAa,QAAQ;AAG7B,KAAI,cAAc,MAAM,WAAW,SAAS,WAAW,OAAO,CAAC,CAC9D,QAAO,MAAM;AAEd,KAAI,gBAAgB,KAAK,SAAS,CACjC,QAAO,MAAM;CAGd,MAAM,EAAE,WAAW,QAAQ;AAC3B,KAAI,CAAC,QAAQ,GACZ,QAAO,MAAM;AAGd,KAAI;EACH,MAAM,OAAO,IAAI,mBAAmB,OAAO,GAAG;EAG9C,MAAM,QAAQ,MAAM,KAAK,eAAe,SAAS;AACjD,MAAI,OAAO;GACV,MAAM,OAAO,MAAM;AACnB,OAAI,KAAK,WAAW,KAAK,IAAI,KAAK,WAAW,MAAM,CAAE,QAAO,MAAM;AAClE,QAAK,UAAU,MAAM,GAAG,CAAC,YAAY,GAAG;GACxC,MAAM,OAAO,eAAe,MAAM,KAAK,GAAG,MAAM,OAAO;AACvD,UAAO,QAAQ,SAAS,MAAM,KAAK;;EAIpC,IAAI,QAAQ,uBAAuB;AACnC,MAAI,CAAC,MAEJ,SAAQ,sBADS,MAAM,KAAK,yBAAyB,CACd;EAGxC,MAAM,eAAe,oBAAoB,OAAO,SAAS;AACzD,MAAI,cAAc;GACjB,MAAM,EAAE,UAAU,gBAAgB;AAClC,OAAI,YAAY,WAAW,KAAK,IAAI,YAAY,WAAW,MAAM,CAAE,QAAO,MAAM;AAChF,QAAK,UAAU,SAAS,GAAG,CAAC,YAAY,GAAG;GAC3C,MAAM,OAAO,eAAe,SAAS,KAAK,GAAG,SAAS,OAAO;AAC7D,UAAO,QAAQ,SAAS,aAAa,KAAK;;EAI3C,MAAM,WAAW,MAAM,MAAM;AAG7B,MAAI,SAAS,WAAW,KAAK;GAC5B,MAAM,WAAW,QAAQ,QAAQ,QAAQ,IAAI,UAAU,IAAI;GAC3D,MAAM,YAAY,QAAQ,QAAQ,QAAQ,IAAI,aAAa,IAAI;AAC/D,QACE,OAAO;IACP,MAAM;IACN;IACA;IACA,CAAC,CACD,YAAY,GAAG;;AAGlB,SAAO;SACA;AAEP,SAAO,MAAM;;EAEb"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../../base64-MBPo9ozB.mjs";
|
|
2
2
|
import { runWithContext } from "../../request-context.mjs";
|
|
3
|
-
import { n as parseContentId, r as verifyPreviewToken } from "../../tokens-
|
|
3
|
+
import { n as parseContentId, r as verifyPreviewToken } from "../../tokens-DKHiCYCB.mjs";
|
|
4
4
|
import { defineMiddleware } from "astro:middleware";
|
|
5
5
|
|
|
6
6
|
//#region src/visual-editing/toolbar.ts
|
|
@@ -522,6 +522,7 @@ function renderToolbar(config) {
|
|
|
522
522
|
// --- Save status tracking ---
|
|
523
523
|
var saveState = "idle"; // idle | unsaved | saving | saved | error
|
|
524
524
|
var saveHideTimer = null;
|
|
525
|
+
var pendingSavePromise = null;
|
|
525
526
|
|
|
526
527
|
function setSaveState(state) {
|
|
527
528
|
saveState = state;
|
|
@@ -616,6 +617,11 @@ function renderToolbar(config) {
|
|
|
616
617
|
|
|
617
618
|
// Publish action
|
|
618
619
|
function publish(collection, id) {
|
|
620
|
+
if (pendingSavePromise) {
|
|
621
|
+
pendingSavePromise.then(function() { publish(collection, id); });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
619
625
|
publishBtn.disabled = true;
|
|
620
626
|
publishBtn.textContent = "Publishing\u2026";
|
|
621
627
|
|
|
@@ -759,7 +765,11 @@ function renderToolbar(config) {
|
|
|
759
765
|
|
|
760
766
|
var newValue = (element.textContent || "").trim();
|
|
761
767
|
if (newValue !== originalText.trim()) {
|
|
762
|
-
saveField(annotation.collection, annotation.id, annotation.field, newValue)
|
|
768
|
+
pendingSavePromise = saveField(annotation.collection, annotation.id, annotation.field, newValue).then(function() {
|
|
769
|
+
pendingSavePromise = null;
|
|
770
|
+
}, function() {
|
|
771
|
+
pendingSavePromise = null;
|
|
772
|
+
});
|
|
763
773
|
} else {
|
|
764
774
|
setSaveState("idle");
|
|
765
775
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-context.mjs","names":[],"sources":["../../../src/visual-editing/toolbar.ts","../../../src/astro/middleware/request-context.ts"],"sourcesContent":["/**\n * EmDash Visual Editing Toolbar\n *\n * A floating pill injected via middleware for authenticated editors.\n * Renders as a plain HTML string with inline styles and a <script> tag.\n * No dependencies — works on any page with a </body> tag.\n */\n\ninterface ToolbarConfig {\n\teditMode: boolean;\n\tisPreview: boolean;\n}\n\nexport function renderToolbar(config: ToolbarConfig): string {\n\tconst { editMode, isPreview } = config;\n\n\treturn `\n<!-- EmDash Visual Editing Toolbar -->\n<div id=\"emdash-toolbar\" data-edit-mode=\"${editMode}\" data-preview=\"${isPreview}\">\n <div class=\"emdash-tb-inner\">\n <span class=\"emdash-tb-logo\">EmDash</span>\n\n <div class=\"emdash-tb-divider\"></div>\n\n <label class=\"emdash-tb-toggle\" title=\"Toggle edit mode\">\n <input type=\"checkbox\" id=\"emdash-edit-toggle\" ${editMode ? \"checked\" : \"\"} />\n <span class=\"emdash-tb-toggle-track\">\n <span class=\"emdash-tb-toggle-thumb\"></span>\n </span>\n <span class=\"emdash-tb-toggle-label\">Edit</span>\n </label>\n\n <span class=\"emdash-tb-status\" id=\"emdash-tb-status\"></span>\n\n <span class=\"emdash-tb-save-status\" id=\"emdash-tb-save-status\"></span>\n\n <a class=\"emdash-tb-admin\" id=\"emdash-tb-admin\" href=\"#\" target=\"emdash-admin\" style=\"display:none\" title=\"Open in admin\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/><polyline points=\"15 3 21 3 21 9\"/><line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/></svg>\n </a>\n\n <button class=\"emdash-tb-publish\" id=\"emdash-tb-publish\" style=\"display:none\">Publish</button>\n </div>\n</div>\n\n<style>\n #emdash-toolbar {\n position: fixed;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 999999;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n font-size: 13px;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n }\n\n .emdash-tb-inner {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 16px;\n background: #1a1a1a;\n color: #e0e0e0;\n border-radius: 999px;\n box-shadow: 0 4px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);\n white-space: nowrap;\n user-select: none;\n }\n\n .emdash-tb-logo {\n font-weight: 600;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: #fff;\n opacity: 0.7;\n }\n\n .emdash-tb-divider {\n width: 1px;\n height: 16px;\n background: rgba(255,255,255,0.15);\n }\n\n /* Toggle switch */\n .emdash-tb-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n }\n\n .emdash-tb-toggle input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .emdash-tb-toggle-track {\n position: relative;\n width: 32px;\n height: 18px;\n background: #444;\n border-radius: 9px;\n transition: background 0.2s;\n }\n\n .emdash-tb-toggle input:checked + .emdash-tb-toggle-track {\n background: #3b82f6;\n }\n\n .emdash-tb-toggle-thumb {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n background: #fff;\n border-radius: 50%;\n transition: transform 0.2s;\n }\n\n .emdash-tb-toggle input:checked + .emdash-tb-toggle-track .emdash-tb-toggle-thumb {\n transform: translateX(14px);\n }\n\n .emdash-tb-toggle-label {\n font-size: 12px;\n color: #aaa;\n }\n\n .emdash-tb-toggle input:checked ~ .emdash-tb-toggle-label {\n color: #fff;\n }\n\n /* Status area — flex for multiple badges */\n .emdash-tb-status {\n display: inline-flex;\n gap: 6px;\n align-items: center;\n }\n\n /* Badges */\n .emdash-tb-badge {\n display: inline-flex;\n align-items: center;\n padding: 3px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n }\n\n .emdash-tb-badge--preview {\n background: rgba(139,92,246,0.2);\n color: #a78bfa;\n }\n\n .emdash-tb-badge--draft {\n background: rgba(245,158,11,0.2);\n color: #fbbf24;\n }\n\n .emdash-tb-badge--published {\n background: rgba(34,197,94,0.2);\n color: #4ade80;\n }\n\n .emdash-tb-badge--pending {\n background: rgba(59,130,246,0.2);\n color: #60a5fa;\n }\n\n .emdash-tb-badge--unsaved {\n background: rgba(245,158,11,0.2);\n color: #fbbf24;\n }\n\n .emdash-tb-badge--saving {\n background: rgba(148,163,184,0.2);\n color: #94a3b8;\n }\n\n .emdash-tb-badge--saved {\n background: rgba(34,197,94,0.2);\n color: #4ade80;\n transition: opacity 0.3s;\n }\n\n .emdash-tb-badge--error {\n background: rgba(239,68,68,0.2);\n color: #f87171;\n }\n\n /* Admin link */\n .emdash-tb-admin {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #888;\n text-decoration: none;\n padding: 2px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n\n .emdash-tb-admin:hover {\n color: #fff;\n }\n\n /* Publish button */\n .emdash-tb-publish {\n padding: 4px 12px;\n background: #3b82f6;\n color: #fff;\n border: none;\n border-radius: 999px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.15s;\n font-family: inherit;\n }\n\n .emdash-tb-publish:hover {\n background: #2563eb;\n }\n\n .emdash-tb-publish:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Edit mode: editable hover styles — uses :has() to check toolbar state */\n body:has(#emdash-toolbar[data-edit-mode=\"true\"]) [data-emdash-ref] {\n transition: box-shadow 0.15s, background-color 0.15s;\n }\n\n body:has(#emdash-toolbar[data-edit-mode=\"true\"]) [data-emdash-ref]:hover {\n box-shadow: 0 0 0 2px rgba(59,130,246,0.5);\n border-radius: 4px;\n background-color: rgba(59,130,246,0.04);\n cursor: text;\n }\n\n /* Active editing state — override hover pencil cursor */\n [data-emdash-editing] {\n box-shadow: 0 0 0 2px #3b82f6 !important;\n border-radius: 4px !important;\n background-color: rgba(59,130,246,0.04) !important;\n cursor: text !important;\n }\n\n /* Suppress browser focus ring on contenteditable and tiptap editor */\n [data-emdash-editing]:focus,\n [data-emdash-ref] .tiptap:focus,\n [data-emdash-ref] .ProseMirror:focus {\n outline: none !important;\n }\n\n /* Image editor popover */\n .emdash-img-popover {\n position: fixed;\n z-index: 1000000;\n background: #1a1a1a;\n border-radius: 12px;\n box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08);\n color: #e0e0e0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n font-size: 13px;\n width: 320px;\n overflow: hidden;\n animation: emdash-img-fadein 0.15s ease-out;\n }\n\n @keyframes emdash-img-fadein {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .emdash-img-popover-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 12px;\n border-bottom: 1px solid rgba(255,255,255,0.08);\n }\n\n .emdash-img-popover-title {\n font-weight: 600;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: #999;\n }\n\n .emdash-img-popover-close {\n background: none;\n border: none;\n color: #666;\n cursor: pointer;\n padding: 2px;\n line-height: 1;\n font-size: 16px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n\n .emdash-img-popover-close:hover {\n color: #fff;\n }\n\n .emdash-img-popover-body {\n padding: 12px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n\n .emdash-img-preview {\n width: 100%;\n max-height: 160px;\n object-fit: contain;\n border-radius: 6px;\n background: #111;\n }\n\n .emdash-img-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 80px;\n border: 2px dashed rgba(255,255,255,0.15);\n border-radius: 6px;\n color: #666;\n font-size: 12px;\n }\n\n .emdash-img-field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .emdash-img-field label {\n font-size: 11px;\n font-weight: 600;\n color: #888;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n\n .emdash-img-field input[type=\"text\"] {\n background: #111;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n color: #e0e0e0;\n padding: 6px 8px;\n font-size: 13px;\n font-family: inherit;\n outline: none;\n transition: border-color 0.15s;\n }\n\n .emdash-img-field input[type=\"text\"]:focus {\n border-color: #3b82f6;\n }\n\n .emdash-img-actions {\n display: flex;\n gap: 6px;\n }\n\n .emdash-img-btn {\n flex: 1;\n padding: 6px 10px;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n background: #222;\n color: #e0e0e0;\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n text-align: center;\n white-space: nowrap;\n }\n\n .emdash-img-btn:hover {\n background: #333;\n border-color: rgba(255,255,255,0.2);\n }\n\n .emdash-img-btn--primary {\n background: #3b82f6;\n border-color: #3b82f6;\n color: #fff;\n }\n\n .emdash-img-btn--primary:hover {\n background: #2563eb;\n border-color: #2563eb;\n }\n\n .emdash-img-btn--danger {\n color: #f87171;\n border-color: rgba(248,113,113,0.3);\n }\n\n .emdash-img-btn--danger:hover {\n background: rgba(248,113,113,0.1);\n border-color: rgba(248,113,113,0.5);\n }\n\n /* Media browser within the popover */\n .emdash-img-browser {\n border-top: 1px solid rgba(255,255,255,0.08);\n padding: 12px;\n }\n\n .emdash-img-browser-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n }\n\n .emdash-img-browser-title {\n font-size: 12px;\n font-weight: 600;\n color: #999;\n }\n\n .emdash-img-browser-back {\n background: none;\n border: none;\n color: #3b82f6;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n padding: 2px 4px;\n }\n\n .emdash-img-browser-back:hover {\n text-decoration: underline;\n }\n\n .emdash-img-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n max-height: 240px;\n overflow-y: auto;\n }\n\n .emdash-img-grid-item {\n aspect-ratio: 1;\n border-radius: 4px;\n overflow: hidden;\n cursor: pointer;\n border: 2px solid transparent;\n transition: border-color 0.15s;\n background: #111;\n }\n\n .emdash-img-grid-item:hover {\n border-color: rgba(59,130,246,0.5);\n }\n\n .emdash-img-grid-item--selected {\n border-color: #3b82f6;\n }\n\n .emdash-img-grid-item img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .emdash-img-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 80px;\n color: #666;\n font-size: 12px;\n }\n\n .emdash-img-drop {\n border: 2px dashed #3b82f6;\n background: rgba(59,130,246,0.05);\n }\n\n .emdash-img-uploading {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 0;\n color: #999;\n font-size: 12px;\n }\n\n .emdash-img-popover-backdrop {\n position: fixed;\n inset: 0;\n z-index: 999999;\n }\n</style>\n\n<script>\n(function() {\n var toolbar = document.getElementById(\"emdash-toolbar\");\n var toggle = document.getElementById(\"emdash-edit-toggle\");\n var statusEl = document.getElementById(\"emdash-tb-status\");\n var saveStatusEl = document.getElementById(\"emdash-tb-save-status\");\n var publishBtn = document.getElementById(\"emdash-tb-publish\");\n if (!toolbar || !toggle || !statusEl || !publishBtn || !saveStatusEl) return;\n\n var isEditMode = toolbar.getAttribute(\"data-edit-mode\") === \"true\";\n\n // CSRF-protected fetch — adds X-EmDash-Request header to all API calls\n function ecFetch(url, init) {\n init = init || {};\n init.headers = Object.assign({ \"X-EmDash-Request\": \"1\" }, init.headers || {});\n return fetch(url, init);\n }\n\n // --- Save status tracking ---\n var saveState = \"idle\"; // idle | unsaved | saving | saved | error\n var saveHideTimer = null;\n\n function setSaveState(state) {\n saveState = state;\n clearTimeout(saveHideTimer);\n\n switch (state) {\n case \"unsaved\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--unsaved\">Unsaved</span>';\n break;\n case \"saving\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--saving\">Saving\\u2026</span>';\n break;\n case \"saved\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--saved\">Saved</span>';\n saveHideTimer = setTimeout(function() {\n saveStatusEl.innerHTML = \"\";\n saveState = \"idle\";\n }, 2000);\n break;\n case \"error\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--error\">Save failed</span>';\n saveHideTimer = setTimeout(function() {\n saveStatusEl.innerHTML = \"\";\n saveState = \"idle\";\n }, 3000);\n break;\n default:\n saveStatusEl.innerHTML = \"\";\n }\n }\n\n // Listen for save events from inline editors (e.g. PT editor)\n document.addEventListener(\"emdash:save\", function(e) {\n var detail = e.detail || {};\n if (detail.state) {\n setSaveState(detail.state);\n }\n });\n\n document.addEventListener(\"emdash:content-changed\", function(e) {\n var detail = e.detail || {};\n if (detail.collection && detail.id) {\n showUnpublishedChanges(detail.collection, detail.id);\n }\n });\n\n // --- Entry status ---\n var entryRef = null;\n\n function updateStatus() {\n if (!isEditMode) {\n statusEl.innerHTML = \"\";\n publishBtn.style.display = \"none\";\n return;\n }\n\n var first = document.querySelector(\"[data-emdash-ref]\");\n if (!first) {\n statusEl.innerHTML = \"\";\n publishBtn.style.display = \"none\";\n return;\n }\n\n try {\n var ref = JSON.parse(first.getAttribute(\"data-emdash-ref\"));\n entryRef = ref;\n if (!ref.status) return;\n\n // Show admin link\n var adminLink = document.getElementById(\"emdash-tb-admin\");\n if (adminLink) {\n adminLink.href = \"/_emdash/admin/content/\" + encodeURIComponent(ref.collection) + \"/\" + encodeURIComponent(ref.id);\n adminLink.style.display = \"\";\n }\n\n if (ref.status === \"draft\") {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--draft\">Draft</span>';\n publishBtn.style.display = \"\";\n publishBtn.onclick = function() { publish(ref.collection, ref.id); };\n } else if (ref.status === \"published\" && ref.hasDraft) {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--pending\">Unpublished changes</span>';\n publishBtn.style.display = \"\";\n publishBtn.onclick = function() { publish(ref.collection, ref.id); };\n } else if (ref.status === \"published\") {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--published\">Published</span>';\n publishBtn.style.display = \"none\";\n }\n } catch (e) {\n // ignore parse errors\n }\n }\n\n // Publish action\n function publish(collection, id) {\n publishBtn.disabled = true;\n publishBtn.textContent = \"Publishing\\u2026\";\n\n ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id) + \"/publish\", {\n method: \"POST\",\n credentials: \"same-origin\",\n })\n .then(function(res) {\n if (res.ok) {\n if (document.startViewTransition) {\n document.startViewTransition(function() { location.reload(); });\n } else {\n location.reload();\n }\n } else {\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n console.error(\"Publish failed:\", res.status);\n }\n })\n .catch(function(err) {\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n console.error(\"Publish failed:\", err);\n });\n }\n\n // Edit mode toggle\n toggle.addEventListener(\"change\", function() {\n if (toggle.checked) {\n document.cookie = \"emdash-edit-mode=true;path=/;samesite=lax\";\n } else {\n document.cookie = \"emdash-edit-mode=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n }\n\n if (document.startViewTransition) {\n document.startViewTransition(function() { location.replace(location.href); });\n } else {\n location.replace(location.href);\n }\n });\n\n // --- Inline editing ---\n\n // Cached manifest (fetched once on first edit click)\n var manifestCache = null;\n var manifestPromise = null;\n\n function fetchManifest() {\n if (manifestCache) return Promise.resolve(manifestCache);\n if (manifestPromise) return manifestPromise;\n manifestPromise = ecFetch(\"/_emdash/api/manifest\", { credentials: \"same-origin\" })\n .then(function(r) { return r.json(); })\n .then(function(m) { manifestCache = m; return m; });\n return manifestPromise;\n }\n\n function getFieldKind(manifest, collection, field) {\n var col = manifest.collections && manifest.collections[collection];\n if (!col || !col.fields) return null;\n var f = col.fields[field];\n return f ? f.kind : null;\n }\n\n // Load manifest early so the first click can resolve field kinds without racing the event.\n if (isEditMode) {\n fetchManifest();\n }\n\n // Save a single field value\n function saveField(collection, id, field, value) {\n setSaveState(\"saving\");\n return ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id), {\n method: \"PUT\",\n credentials: \"same-origin\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: { [field]: value } }),\n })\n .then(function(res) {\n if (res.ok) {\n setSaveState(\"saved\");\n // A save creates/updates a draft — show unpublished changes\n showUnpublishedChanges(collection, id);\n } else {\n setSaveState(\"error\");\n console.error(\"Save failed:\", res.status);\n }\n })\n .catch(function(err) {\n setSaveState(\"error\");\n console.error(\"Save failed:\", err);\n });\n }\n\n function showUnpublishedChanges(collection, id) {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--pending\">Unpublished changes</span>';\n publishBtn.style.display = \"\";\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n publishBtn.onclick = function() { publish(collection, id); };\n }\n\n // Plain text inline editing (contenteditable)\n var currentlyEditing = null;\n\n function startTextEdit(element, annotation) {\n if (currentlyEditing === element) return;\n if (currentlyEditing) endCurrentEdit();\n\n currentlyEditing = element;\n var originalText = element.textContent || \"\";\n\n element.setAttribute(\"data-emdash-editing\", \"\");\n element.contentEditable = \"plaintext-only\";\n element.focus();\n\n // Select all text\n var range = document.createRange();\n range.selectNodeContents(element);\n var sel = window.getSelection();\n sel.removeAllRanges();\n sel.addRange(range);\n\n // Track dirty state via input events\n function handleInput() {\n var current = (element.textContent || \"\").trim();\n if (current !== originalText.trim()) {\n setSaveState(\"unsaved\");\n } else {\n setSaveState(\"idle\");\n }\n }\n\n function handleBlur() {\n element.removeEventListener(\"blur\", handleBlur);\n element.removeEventListener(\"keydown\", handleKeydown);\n element.removeEventListener(\"input\", handleInput);\n element.contentEditable = \"false\";\n element.removeAttribute(\"data-emdash-editing\");\n currentlyEditing = null;\n\n var newValue = (element.textContent || \"\").trim();\n if (newValue !== originalText.trim()) {\n saveField(annotation.collection, annotation.id, annotation.field, newValue);\n } else {\n setSaveState(\"idle\");\n }\n }\n\n function handleKeydown(e) {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n element.blur();\n }\n if (e.key === \"Escape\") {\n element.textContent = originalText;\n setSaveState(\"idle\");\n element.blur();\n }\n }\n\n element.addEventListener(\"input\", handleInput);\n element.addEventListener(\"blur\", handleBlur);\n element.addEventListener(\"keydown\", handleKeydown);\n }\n\n function endCurrentEdit() {\n if (currentlyEditing) {\n currentlyEditing.blur();\n }\n }\n\n // Fallback: open admin\n function openAdmin(annotation) {\n var url = \"/_emdash/admin/content/\" + encodeURIComponent(annotation.collection) + \"/\" + encodeURIComponent(annotation.id);\n if (annotation.field) {\n url += \"?field=\" + encodeURIComponent(annotation.field);\n }\n window.open(url, \"emdash-admin\");\n }\n\n // --- Inline image editing ---\n var activeImagePopover = null;\n\n function closeImagePopover() {\n if (activeImagePopover) {\n activeImagePopover.backdrop.remove();\n activeImagePopover.popover.remove();\n if (activeImagePopover.escapeHandler) {\n document.removeEventListener(\"keydown\", activeImagePopover.escapeHandler);\n }\n activeImagePopover = null;\n }\n }\n\n function startImageEdit(element, annotation) {\n closeImagePopover();\n\n // Find the current image value by fetching the entry\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Find img element inside the annotated container (or the element itself if it's an img)\n var imgEl = element.tagName === \"IMG\" ? element : element.querySelector(\"img\");\n\n // Fetch current field value from the content API\n ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id), {\n credentials: \"same-origin\"\n })\n .then(function(r) { return r.json(); })\n .then(function(entry) {\n var currentValue = entry.data && entry.data[field];\n showImagePopover(element, imgEl, annotation, currentValue);\n })\n .catch(function() {\n // If fetch fails, still show popover with what we can infer from DOM\n showImagePopover(element, imgEl, annotation, null);\n });\n }\n\n function showImagePopover(element, imgEl, annotation, currentValue) {\n closeImagePopover();\n\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Position near the element\n var rect = element.getBoundingClientRect();\n var viewportH = window.innerHeight;\n var viewportW = window.innerWidth;\n\n // Create backdrop for click-outside-to-close\n var backdrop = document.createElement(\"div\");\n backdrop.className = \"emdash-img-popover-backdrop\";\n backdrop.addEventListener(\"click\", function(e) {\n if (e.target === backdrop) closeImagePopover();\n });\n\n // Create popover\n var popover = document.createElement(\"div\");\n popover.className = \"emdash-img-popover\";\n\n var currentSrc = currentValue ? (currentValue.previewUrl || currentValue.src) : (imgEl ? imgEl.src : null);\n var currentAlt = currentValue ? (currentValue.alt || \"\") : (imgEl ? (imgEl.alt || \"\") : \"\");\n\n // Build popover HTML\n var html = '';\n html += '<div class=\"emdash-img-popover-header\">';\n html += ' <span class=\"emdash-img-popover-title\">Image</span>';\n html += ' <button class=\"emdash-img-popover-close\" data-action=\"close\">×</button>';\n html += '</div>';\n html += '<div class=\"emdash-img-popover-body\" id=\"emdash-img-main\">';\n\n if (currentSrc) {\n html += '<img class=\"emdash-img-preview\" src=\"' + escapeAttr(currentSrc) + '\" alt=\"\" />';\n } else {\n html += '<div class=\"emdash-img-empty\">No image selected</div>';\n }\n\n html += '<div class=\"emdash-img-field\">';\n html += ' <label for=\"emdash-img-alt\">Alt text</label>';\n html += ' <input type=\"text\" id=\"emdash-img-alt\" value=\"' + escapeAttr(currentAlt) + '\" placeholder=\"Describe the image\" />';\n html += '</div>';\n\n html += '<div class=\"emdash-img-actions\">';\n html += ' <button class=\"emdash-img-btn emdash-img-btn--primary\" data-action=\"browse\">Replace</button>';\n html += ' <label class=\"emdash-img-btn\" style=\"cursor:pointer\">';\n html += ' Upload';\n html += ' <input type=\"file\" accept=\"image/*\" id=\"emdash-img-upload\" style=\"display:none\" />';\n html += ' </label>';\n if (currentSrc) {\n html += ' <button class=\"emdash-img-btn emdash-img-btn--danger\" data-action=\"remove\">Remove</button>';\n }\n html += '</div>';\n html += '</div>';\n\n popover.innerHTML = html;\n\n backdrop.appendChild(popover);\n document.body.appendChild(backdrop);\n\n // Position the popover\n positionPopover(popover, rect, viewportW, viewportH);\n\n // Escape key handler\n function handleEscape(e) {\n if (e.key === \"Escape\") {\n closeImagePopover();\n document.removeEventListener(\"keydown\", handleEscape);\n }\n }\n document.addEventListener(\"keydown\", handleEscape);\n\n activeImagePopover = {\n backdrop: backdrop,\n popover: popover,\n annotation: annotation,\n currentValue: currentValue,\n element: element,\n imgEl: imgEl,\n escapeHandler: handleEscape\n };\n\n // Event handlers\n popover.querySelector('[data-action=\"close\"]').addEventListener(\"click\", closeImagePopover);\n\n popover.querySelector('[data-action=\"browse\"]').addEventListener(\"click\", function() {\n showMediaBrowser(popover, annotation, currentValue, element, imgEl);\n });\n\n var uploadInput = popover.querySelector(\"#emdash-img-upload\");\n uploadInput.addEventListener(\"change\", function(e) {\n var file = e.target.files && e.target.files[0];\n if (file) handleImageUpload(file, popover, annotation, element, imgEl);\n });\n\n var removeBtn = popover.querySelector('[data-action=\"remove\"]');\n if (removeBtn) {\n removeBtn.addEventListener(\"click\", function() {\n saveField(collection, id, field, null).then(function() {\n if (imgEl) {\n imgEl.style.display = \"none\";\n }\n closeImagePopover();\n });\n });\n }\n\n // Save alt text on change (debounced)\n var altInput = popover.querySelector(\"#emdash-img-alt\");\n var altTimer = null;\n altInput.addEventListener(\"input\", function() {\n clearTimeout(altTimer);\n altTimer = setTimeout(function() {\n var newAlt = altInput.value;\n if (currentValue) {\n var updated = Object.assign({}, currentValue, { alt: newAlt });\n saveField(collection, id, field, updated);\n if (imgEl) imgEl.alt = newAlt;\n }\n }, 500);\n });\n\n // Handle drag and drop on the popover body\n var body = popover.querySelector(\".emdash-img-popover-body\");\n body.addEventListener(\"dragover\", function(e) {\n e.preventDefault();\n body.classList.add(\"emdash-img-drop\");\n });\n body.addEventListener(\"dragleave\", function() {\n body.classList.remove(\"emdash-img-drop\");\n });\n body.addEventListener(\"drop\", function(e) {\n e.preventDefault();\n body.classList.remove(\"emdash-img-drop\");\n var file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];\n if (file && file.type.startsWith(\"image/\")) {\n handleImageUpload(file, popover, annotation, element, imgEl);\n }\n });\n }\n\n function positionPopover(popover, targetRect, viewportW, viewportH) {\n var popoverW = 320;\n var gap = 8;\n\n // Try to place to the right of the element\n var left = targetRect.right + gap;\n var top = targetRect.top;\n\n // If it overflows right, place to the left\n if (left + popoverW > viewportW - 16) {\n left = targetRect.left - popoverW - gap;\n }\n // If it still overflows (narrow viewport), center below\n if (left < 16) {\n left = Math.max(16, (viewportW - popoverW) / 2);\n top = targetRect.bottom + gap;\n }\n // Clamp vertically\n if (top + 400 > viewportH - 80) { // 80 for toolbar\n top = Math.max(16, viewportH - 480);\n }\n if (top < 16) top = 16;\n\n popover.style.left = left + \"px\";\n popover.style.top = top + \"px\";\n }\n\n function escapeAttr(str) {\n return String(str || \"\").replace(/&/g, \"&\").replace(/\"/g, \""\").replace(/</g, \"<\").replace(/>/g, \">\");\n }\n\n function showMediaBrowser(popover, annotation, currentValue, element, imgEl) {\n var mainBody = popover.querySelector(\"#emdash-img-main\");\n if (mainBody) mainBody.style.display = \"none\";\n\n // Remove existing browser if any\n var existing = popover.querySelector(\".emdash-img-browser\");\n if (existing) existing.remove();\n\n var browser = document.createElement(\"div\");\n browser.className = \"emdash-img-browser\";\n\n browser.innerHTML = '<div class=\"emdash-img-browser-header\">' +\n '<span class=\"emdash-img-browser-title\">Media Library</span>' +\n '<button class=\"emdash-img-browser-back\">Back</button>' +\n '</div>' +\n '<div class=\"emdash-img-loading\">Loading\\u2026</div>';\n\n popover.appendChild(browser);\n\n browser.querySelector(\".emdash-img-browser-back\").addEventListener(\"click\", function() {\n browser.remove();\n if (mainBody) mainBody.style.display = \"\";\n });\n\n // Fetch media\n ecFetch(\"/_emdash/api/media?mimeType=image/&limit=30\", { credentials: \"same-origin\" })\n .then(function(r) { return r.json(); })\n .then(function(data) {\n var items = data.items || [];\n var loadingEl = browser.querySelector(\".emdash-img-loading\");\n if (loadingEl) loadingEl.remove();\n\n if (items.length === 0) {\n var empty = document.createElement(\"div\");\n empty.className = \"emdash-img-loading\";\n empty.textContent = \"No images found\";\n browser.appendChild(empty);\n return;\n }\n\n var grid = document.createElement(\"div\");\n grid.className = \"emdash-img-grid\";\n\n items.forEach(function(item) {\n var thumb = document.createElement(\"div\");\n thumb.className = \"emdash-img-grid-item\";\n if (currentValue && currentValue.id === item.id) {\n thumb.classList.add(\"emdash-img-grid-item--selected\");\n }\n var thumbUrl = item.url || item.previewUrl || (\"/_emdash/api/media/file/\" + item.storageKey);\n thumb.innerHTML = '<img src=\"' + escapeAttr(thumbUrl) + '\" alt=\"' + escapeAttr(item.alt || item.filename || \"\") + '\" loading=\"lazy\" />';\n\n thumb.addEventListener(\"click\", function() {\n selectMediaItem(item, annotation, element, imgEl);\n });\n\n grid.appendChild(thumb);\n });\n\n browser.appendChild(grid);\n })\n .catch(function(err) {\n var loadingEl = browser.querySelector(\".emdash-img-loading\");\n if (loadingEl) loadingEl.textContent = \"Failed to load media\";\n console.error(\"Media fetch error:\", err);\n });\n }\n\n function selectMediaItem(item, annotation, element, imgEl) {\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n var isLocal = !item.provider || item.provider === \"local\";\n var itemUrl = item.url || item.previewUrl || (\"/_emdash/api/media/file/\" + item.storageKey);\n\n var newValue = {\n id: item.id,\n provider: item.provider || \"local\",\n src: isLocal ? itemUrl : undefined,\n previewUrl: isLocal ? undefined : itemUrl,\n alt: item.alt || \"\",\n width: item.width,\n height: item.height,\n meta: item.meta\n };\n\n // Clean undefined fields\n Object.keys(newValue).forEach(function(k) {\n if (newValue[k] === undefined) delete newValue[k];\n });\n\n saveField(collection, id, field, newValue).then(function() {\n // Update the image in the DOM\n if (imgEl) {\n imgEl.src = itemUrl;\n imgEl.alt = item.alt || \"\";\n imgEl.style.display = \"\";\n }\n closeImagePopover();\n });\n }\n\n function handleImageUpload(file, popover, annotation, element, imgEl) {\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Show uploading state\n var mainBody = popover.querySelector(\"#emdash-img-main\");\n var browserEl = popover.querySelector(\".emdash-img-browser\");\n if (browserEl) browserEl.remove();\n if (mainBody) {\n mainBody.innerHTML = '<div class=\"emdash-img-uploading\">' +\n '<span>Uploading ' + escapeAttr(file.name) + '\\u2026</span>' +\n '</div>';\n mainBody.style.display = \"\";\n }\n\n // Detect dimensions before upload\n var dimPromise = new Promise(function(resolve) {\n if (!file.type.startsWith(\"image/\")) return resolve({});\n var img = new Image();\n img.onload = function() {\n resolve({ width: img.naturalWidth, height: img.naturalHeight });\n URL.revokeObjectURL(img.src);\n };\n img.onerror = function() {\n resolve({});\n URL.revokeObjectURL(img.src);\n };\n img.src = URL.createObjectURL(file);\n });\n\n dimPromise.then(function(dims) {\n // Generate a thumbnail for large images to avoid OOM in server-side\n // blurhash generation on memory-constrained runtimes (Workers).\n // Thumbnail fits within a 64x64 box (scale by max dimension) so that\n // extreme aspect ratios don't explode into a huge canvas client-side.\n var thumbPromise;\n if (dims.width && dims.height && dims.width * dims.height * 4 > 32 * 1024 * 1024) {\n thumbPromise = new Promise(function(resolve) {\n try {\n var maxDim = Math.max(dims.width, dims.height);\n var scale = Math.min(1, 64 / maxDim);\n var thumbW = Math.max(1, Math.round(dims.width * scale));\n var thumbH = Math.max(1, Math.round(dims.height * scale));\n var canvas = document.createElement(\"canvas\");\n canvas.width = thumbW;\n canvas.height = thumbH;\n var ctx = canvas.getContext(\"2d\");\n if (ctx) {\n var img = new Image();\n img.onload = function() {\n try {\n ctx.drawImage(img, 0, 0, thumbW, thumbH);\n canvas.toBlob(function(blob) {\n URL.revokeObjectURL(img.src);\n resolve(blob);\n }, \"image/png\");\n } catch (e) {\n URL.revokeObjectURL(img.src);\n resolve(null);\n }\n };\n img.onerror = function() {\n URL.revokeObjectURL(img.src);\n resolve(null);\n };\n img.src = URL.createObjectURL(file);\n } else {\n resolve(null);\n }\n } catch (e) {\n resolve(null);\n }\n });\n } else {\n thumbPromise = Promise.resolve(null);\n }\n\n return thumbPromise.then(function(thumbnail) {\n var formData = new FormData();\n formData.append(\"file\", file);\n if (dims.width) formData.append(\"width\", String(dims.width));\n if (dims.height) formData.append(\"height\", String(dims.height));\n if (thumbnail) formData.append(\"thumbnail\", thumbnail, \"thumb.png\");\n\n return ecFetch(\"/_emdash/api/media\", {\n method: \"POST\",\n credentials: \"same-origin\",\n body: formData\n });\n });\n })\n .then(function(r) { return r.json(); })\n .then(function(data) {\n if (!data.item) throw new Error(\"Upload failed\");\n var item = data.item;\n selectMediaItem(item, annotation, element, imgEl);\n })\n .catch(function(err) {\n console.error(\"Upload error:\", err);\n setSaveState(\"error\");\n closeImagePopover();\n });\n }\n\n // Click handler for edit mode\n if (isEditMode) {\n document.addEventListener(\"click\", function(e) {\n var target = e.target;\n\n // Don't intercept clicks on elements currently being edited\n if (target.hasAttribute && target.hasAttribute(\"data-emdash-editing\")) return;\n\n // Walk up to find annotated element\n while (target && target !== document.body) {\n if (target.hasAttribute && target.hasAttribute(\"data-emdash-editing\")) return;\n\n var ref = target.getAttribute && target.getAttribute(\"data-emdash-ref\");\n if (ref) {\n try {\n var annotation = JSON.parse(ref);\n\n // Entry-level annotation (no field) — keep walking for a field-level ancestor\n if (!annotation.field) {\n target = target.parentElement;\n continue;\n }\n\n function dispatchInline(kind) {\n closeImagePopover();\n // Portable Text is edited in-page by InlinePortableTextEditor — do not open admin\n if (kind === \"portableText\") {\n return;\n }\n e.preventDefault();\n e.stopPropagation();\n if (kind === \"string\" || kind === \"text\") {\n startTextEdit(target, annotation);\n } else if (kind === \"image\") {\n startImageEdit(target, annotation);\n } else {\n openAdmin(annotation);\n }\n }\n\n if (manifestCache) {\n dispatchInline(getFieldKind(manifestCache, annotation.collection, annotation.field));\n } else {\n fetchManifest().then(function(manifest) {\n dispatchInline(getFieldKind(manifest, annotation.collection, annotation.field));\n });\n }\n } catch (err) {\n console.error(\"Failed to parse emdash ref:\", err);\n }\n return;\n }\n target = target.parentElement;\n }\n }, true);\n }\n\n updateStatus();\n})();\n</script>\n`;\n}\n","/**\n * EmDash Request Context Middleware\n *\n * Sets up AsyncLocalStorage-based request context for query functions.\n * Skips ALS entirely for logged-out users with no CMS signals (fast path).\n *\n * Handles:\n * - Preview tokens: _preview query param with signed HMAC token\n * - Edit mode: emdash-edit-mode cookie (for visual editing)\n * - Toolbar injection: floating pill for authenticated editors\n */\n\nimport { defineMiddleware } from \"astro:middleware\";\n\nimport { verifyPreviewToken, parseContentId } from \"../../preview/tokens.js\";\nimport { runWithContext } from \"../../request-context.js\";\nimport { renderToolbar } from \"../../visual-editing/toolbar.js\";\n\n/**\n * Inject toolbar HTML into a response if it's an HTML page.\n * Returns the original response if not HTML.\n */\nasync function injectToolbar(response: Response, toolbarHtml: string): Promise<Response> {\n\tconst contentType = response.headers.get(\"content-type\");\n\tif (!contentType?.includes(\"text/html\")) return response;\n\n\tconst html = await response.text();\n\tif (!html.includes(\"</body>\")) return new Response(html, response);\n\n\tconst injected = html.replace(\"</body>\", `${toolbarHtml}</body>`);\n\treturn new Response(injected, {\n\t\tstatus: response.status,\n\t\theaders: response.headers,\n\t});\n}\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { cookies, url } = context;\n\n\t// Skip /_emdash routes (admin has its own UI, no rendering context needed)\n\tif (url.pathname.startsWith(\"/_emdash\")) {\n\t\treturn next();\n\t}\n\n\t// Check for authenticated editor (role >= 30)\n\tconst { user } = context.locals;\n\tconst isEditor = !!user && user.role >= 30;\n\n\t// Playground mode: the playground middleware (from @emdash-cms/cloudflare) stashes\n\t// the per-session DO database on locals.__playgroundDb. We set it via ALS here\n\t// (same module instance as the loader) so getDb() picks it up correctly.\n\tconst playgroundDb = context.locals.__playgroundDb;\n\tif (playgroundDb) {\n\t\t// Check if playground user has toggled edit mode on\n\t\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\treturn runWithContext({ editMode: hasEditCookie, db: playgroundDb }, () => next());\n\t}\n\n\t// Fast path: check for CMS signals before doing any work\n\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\tconst hasPreviewToken = url.searchParams.has(\"_preview\");\n\n\t// No CMS signals and not an editor → skip everything (zero overhead)\n\tif (!hasEditCookie && !hasPreviewToken && !isEditor) {\n\t\treturn next();\n\t}\n\n\t// Determine edit mode: cookie AND authenticated editor\n\tconst editMode = hasEditCookie && isEditor;\n\n\t// Read locale from Astro's i18n routing\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Astro context includes currentLocale when i18n is configured\n\tconst locale = (context as { currentLocale?: string }).currentLocale;\n\n\t// Verify preview token if present\n\tlet preview: { collection: string; id: string } | undefined;\n\tif (hasPreviewToken) {\n\t\tconst secret = import.meta.env.EMDASH_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET || \"\";\n\n\t\tif (secret) {\n\t\t\tconst result = await verifyPreviewToken({ url, secret });\n\t\t\tif (result.valid) {\n\t\t\t\tconst { collection, id } = parseContentId(result.payload.cid);\n\t\t\t\tpreview = { collection, id };\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we have CMS signals, wrap in ALS context\n\tconst needsContext = hasEditCookie || hasPreviewToken;\n\n\tif (needsContext) {\n\t\treturn runWithContext({ editMode, preview, locale }, async () => {\n\t\t\tlet response = await next();\n\n\t\t\t// Preview responses must not be cached -- draft content could leak past token expiry.\n\t\t\t// Clone the response before modifying headers — the original may be immutable.\n\t\t\tif (preview) {\n\t\t\t\tresponse = new Response(response.body, response);\n\t\t\t\tresponse.headers.set(\"Cache-Control\", \"private, no-store\");\n\t\t\t}\n\n\t\t\t// Inject toolbar for authenticated editors\n\t\t\tif (isEditor) {\n\t\t\t\tconst toolbarHtml = renderToolbar({\n\t\t\t\t\teditMode,\n\t\t\t\t\tisPreview: !!preview,\n\t\t\t\t});\n\t\t\t\treturn injectToolbar(response, toolbarHtml);\n\t\t\t}\n\n\t\t\treturn response;\n\t\t});\n\t}\n\n\t// Editor without CMS signals — no ALS needed, but inject toolbar\n\tif (isEditor) {\n\t\tconst response = await next();\n\t\tconst toolbarHtml = renderToolbar({\n\t\t\teditMode: false,\n\t\t\tisPreview: false,\n\t\t});\n\t\treturn injectToolbar(response, toolbarHtml);\n\t}\n\n\treturn next();\n});\n\nexport default onRequest;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"request-context.mjs","names":[],"sources":["../../../src/visual-editing/toolbar.ts","../../../src/astro/middleware/request-context.ts"],"sourcesContent":["/**\n * EmDash Visual Editing Toolbar\n *\n * A floating pill injected via middleware for authenticated editors.\n * Renders as a plain HTML string with inline styles and a <script> tag.\n * No dependencies — works on any page with a </body> tag.\n */\n\ninterface ToolbarConfig {\n\teditMode: boolean;\n\tisPreview: boolean;\n}\n\nexport function renderToolbar(config: ToolbarConfig): string {\n\tconst { editMode, isPreview } = config;\n\n\treturn `\n<!-- EmDash Visual Editing Toolbar -->\n<div id=\"emdash-toolbar\" data-edit-mode=\"${editMode}\" data-preview=\"${isPreview}\">\n <div class=\"emdash-tb-inner\">\n <span class=\"emdash-tb-logo\">EmDash</span>\n\n <div class=\"emdash-tb-divider\"></div>\n\n <label class=\"emdash-tb-toggle\" title=\"Toggle edit mode\">\n <input type=\"checkbox\" id=\"emdash-edit-toggle\" ${editMode ? \"checked\" : \"\"} />\n <span class=\"emdash-tb-toggle-track\">\n <span class=\"emdash-tb-toggle-thumb\"></span>\n </span>\n <span class=\"emdash-tb-toggle-label\">Edit</span>\n </label>\n\n <span class=\"emdash-tb-status\" id=\"emdash-tb-status\"></span>\n\n <span class=\"emdash-tb-save-status\" id=\"emdash-tb-save-status\"></span>\n\n <a class=\"emdash-tb-admin\" id=\"emdash-tb-admin\" href=\"#\" target=\"emdash-admin\" style=\"display:none\" title=\"Open in admin\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/><polyline points=\"15 3 21 3 21 9\"/><line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\"/></svg>\n </a>\n\n <button class=\"emdash-tb-publish\" id=\"emdash-tb-publish\" style=\"display:none\">Publish</button>\n </div>\n</div>\n\n<style>\n #emdash-toolbar {\n position: fixed;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 999999;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n font-size: 13px;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n }\n\n .emdash-tb-inner {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 16px;\n background: #1a1a1a;\n color: #e0e0e0;\n border-radius: 999px;\n box-shadow: 0 4px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);\n white-space: nowrap;\n user-select: none;\n }\n\n .emdash-tb-logo {\n font-weight: 600;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: #fff;\n opacity: 0.7;\n }\n\n .emdash-tb-divider {\n width: 1px;\n height: 16px;\n background: rgba(255,255,255,0.15);\n }\n\n /* Toggle switch */\n .emdash-tb-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n }\n\n .emdash-tb-toggle input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .emdash-tb-toggle-track {\n position: relative;\n width: 32px;\n height: 18px;\n background: #444;\n border-radius: 9px;\n transition: background 0.2s;\n }\n\n .emdash-tb-toggle input:checked + .emdash-tb-toggle-track {\n background: #3b82f6;\n }\n\n .emdash-tb-toggle-thumb {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n background: #fff;\n border-radius: 50%;\n transition: transform 0.2s;\n }\n\n .emdash-tb-toggle input:checked + .emdash-tb-toggle-track .emdash-tb-toggle-thumb {\n transform: translateX(14px);\n }\n\n .emdash-tb-toggle-label {\n font-size: 12px;\n color: #aaa;\n }\n\n .emdash-tb-toggle input:checked ~ .emdash-tb-toggle-label {\n color: #fff;\n }\n\n /* Status area — flex for multiple badges */\n .emdash-tb-status {\n display: inline-flex;\n gap: 6px;\n align-items: center;\n }\n\n /* Badges */\n .emdash-tb-badge {\n display: inline-flex;\n align-items: center;\n padding: 3px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n }\n\n .emdash-tb-badge--preview {\n background: rgba(139,92,246,0.2);\n color: #a78bfa;\n }\n\n .emdash-tb-badge--draft {\n background: rgba(245,158,11,0.2);\n color: #fbbf24;\n }\n\n .emdash-tb-badge--published {\n background: rgba(34,197,94,0.2);\n color: #4ade80;\n }\n\n .emdash-tb-badge--pending {\n background: rgba(59,130,246,0.2);\n color: #60a5fa;\n }\n\n .emdash-tb-badge--unsaved {\n background: rgba(245,158,11,0.2);\n color: #fbbf24;\n }\n\n .emdash-tb-badge--saving {\n background: rgba(148,163,184,0.2);\n color: #94a3b8;\n }\n\n .emdash-tb-badge--saved {\n background: rgba(34,197,94,0.2);\n color: #4ade80;\n transition: opacity 0.3s;\n }\n\n .emdash-tb-badge--error {\n background: rgba(239,68,68,0.2);\n color: #f87171;\n }\n\n /* Admin link */\n .emdash-tb-admin {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: #888;\n text-decoration: none;\n padding: 2px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n\n .emdash-tb-admin:hover {\n color: #fff;\n }\n\n /* Publish button */\n .emdash-tb-publish {\n padding: 4px 12px;\n background: #3b82f6;\n color: #fff;\n border: none;\n border-radius: 999px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: background 0.15s;\n font-family: inherit;\n }\n\n .emdash-tb-publish:hover {\n background: #2563eb;\n }\n\n .emdash-tb-publish:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Edit mode: editable hover styles — uses :has() to check toolbar state */\n body:has(#emdash-toolbar[data-edit-mode=\"true\"]) [data-emdash-ref] {\n transition: box-shadow 0.15s, background-color 0.15s;\n }\n\n body:has(#emdash-toolbar[data-edit-mode=\"true\"]) [data-emdash-ref]:hover {\n box-shadow: 0 0 0 2px rgba(59,130,246,0.5);\n border-radius: 4px;\n background-color: rgba(59,130,246,0.04);\n cursor: text;\n }\n\n /* Active editing state — override hover pencil cursor */\n [data-emdash-editing] {\n box-shadow: 0 0 0 2px #3b82f6 !important;\n border-radius: 4px !important;\n background-color: rgba(59,130,246,0.04) !important;\n cursor: text !important;\n }\n\n /* Suppress browser focus ring on contenteditable and tiptap editor */\n [data-emdash-editing]:focus,\n [data-emdash-ref] .tiptap:focus,\n [data-emdash-ref] .ProseMirror:focus {\n outline: none !important;\n }\n\n /* Image editor popover */\n .emdash-img-popover {\n position: fixed;\n z-index: 1000000;\n background: #1a1a1a;\n border-radius: 12px;\n box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.08);\n color: #e0e0e0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n font-size: 13px;\n width: 320px;\n overflow: hidden;\n animation: emdash-img-fadein 0.15s ease-out;\n }\n\n @keyframes emdash-img-fadein {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .emdash-img-popover-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 12px;\n border-bottom: 1px solid rgba(255,255,255,0.08);\n }\n\n .emdash-img-popover-title {\n font-weight: 600;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: #999;\n }\n\n .emdash-img-popover-close {\n background: none;\n border: none;\n color: #666;\n cursor: pointer;\n padding: 2px;\n line-height: 1;\n font-size: 16px;\n border-radius: 4px;\n transition: color 0.15s;\n }\n\n .emdash-img-popover-close:hover {\n color: #fff;\n }\n\n .emdash-img-popover-body {\n padding: 12px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n\n .emdash-img-preview {\n width: 100%;\n max-height: 160px;\n object-fit: contain;\n border-radius: 6px;\n background: #111;\n }\n\n .emdash-img-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 80px;\n border: 2px dashed rgba(255,255,255,0.15);\n border-radius: 6px;\n color: #666;\n font-size: 12px;\n }\n\n .emdash-img-field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .emdash-img-field label {\n font-size: 11px;\n font-weight: 600;\n color: #888;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n\n .emdash-img-field input[type=\"text\"] {\n background: #111;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n color: #e0e0e0;\n padding: 6px 8px;\n font-size: 13px;\n font-family: inherit;\n outline: none;\n transition: border-color 0.15s;\n }\n\n .emdash-img-field input[type=\"text\"]:focus {\n border-color: #3b82f6;\n }\n\n .emdash-img-actions {\n display: flex;\n gap: 6px;\n }\n\n .emdash-img-btn {\n flex: 1;\n padding: 6px 10px;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n background: #222;\n color: #e0e0e0;\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n text-align: center;\n white-space: nowrap;\n }\n\n .emdash-img-btn:hover {\n background: #333;\n border-color: rgba(255,255,255,0.2);\n }\n\n .emdash-img-btn--primary {\n background: #3b82f6;\n border-color: #3b82f6;\n color: #fff;\n }\n\n .emdash-img-btn--primary:hover {\n background: #2563eb;\n border-color: #2563eb;\n }\n\n .emdash-img-btn--danger {\n color: #f87171;\n border-color: rgba(248,113,113,0.3);\n }\n\n .emdash-img-btn--danger:hover {\n background: rgba(248,113,113,0.1);\n border-color: rgba(248,113,113,0.5);\n }\n\n /* Media browser within the popover */\n .emdash-img-browser {\n border-top: 1px solid rgba(255,255,255,0.08);\n padding: 12px;\n }\n\n .emdash-img-browser-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n }\n\n .emdash-img-browser-title {\n font-size: 12px;\n font-weight: 600;\n color: #999;\n }\n\n .emdash-img-browser-back {\n background: none;\n border: none;\n color: #3b82f6;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n padding: 2px 4px;\n }\n\n .emdash-img-browser-back:hover {\n text-decoration: underline;\n }\n\n .emdash-img-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n max-height: 240px;\n overflow-y: auto;\n }\n\n .emdash-img-grid-item {\n aspect-ratio: 1;\n border-radius: 4px;\n overflow: hidden;\n cursor: pointer;\n border: 2px solid transparent;\n transition: border-color 0.15s;\n background: #111;\n }\n\n .emdash-img-grid-item:hover {\n border-color: rgba(59,130,246,0.5);\n }\n\n .emdash-img-grid-item--selected {\n border-color: #3b82f6;\n }\n\n .emdash-img-grid-item img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .emdash-img-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 80px;\n color: #666;\n font-size: 12px;\n }\n\n .emdash-img-drop {\n border: 2px dashed #3b82f6;\n background: rgba(59,130,246,0.05);\n }\n\n .emdash-img-uploading {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 0;\n color: #999;\n font-size: 12px;\n }\n\n .emdash-img-popover-backdrop {\n position: fixed;\n inset: 0;\n z-index: 999999;\n }\n</style>\n\n<script>\n(function() {\n var toolbar = document.getElementById(\"emdash-toolbar\");\n var toggle = document.getElementById(\"emdash-edit-toggle\");\n var statusEl = document.getElementById(\"emdash-tb-status\");\n var saveStatusEl = document.getElementById(\"emdash-tb-save-status\");\n var publishBtn = document.getElementById(\"emdash-tb-publish\");\n if (!toolbar || !toggle || !statusEl || !publishBtn || !saveStatusEl) return;\n\n var isEditMode = toolbar.getAttribute(\"data-edit-mode\") === \"true\";\n\n // CSRF-protected fetch — adds X-EmDash-Request header to all API calls\n function ecFetch(url, init) {\n init = init || {};\n init.headers = Object.assign({ \"X-EmDash-Request\": \"1\" }, init.headers || {});\n return fetch(url, init);\n }\n\n // --- Save status tracking ---\n var saveState = \"idle\"; // idle | unsaved | saving | saved | error\n var saveHideTimer = null;\n var pendingSavePromise = null;\n\n function setSaveState(state) {\n saveState = state;\n clearTimeout(saveHideTimer);\n\n switch (state) {\n case \"unsaved\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--unsaved\">Unsaved</span>';\n break;\n case \"saving\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--saving\">Saving\\u2026</span>';\n break;\n case \"saved\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--saved\">Saved</span>';\n saveHideTimer = setTimeout(function() {\n saveStatusEl.innerHTML = \"\";\n saveState = \"idle\";\n }, 2000);\n break;\n case \"error\":\n saveStatusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--error\">Save failed</span>';\n saveHideTimer = setTimeout(function() {\n saveStatusEl.innerHTML = \"\";\n saveState = \"idle\";\n }, 3000);\n break;\n default:\n saveStatusEl.innerHTML = \"\";\n }\n }\n\n // Listen for save events from inline editors (e.g. PT editor)\n document.addEventListener(\"emdash:save\", function(e) {\n var detail = e.detail || {};\n if (detail.state) {\n setSaveState(detail.state);\n }\n });\n\n document.addEventListener(\"emdash:content-changed\", function(e) {\n var detail = e.detail || {};\n if (detail.collection && detail.id) {\n showUnpublishedChanges(detail.collection, detail.id);\n }\n });\n\n // --- Entry status ---\n var entryRef = null;\n\n function updateStatus() {\n if (!isEditMode) {\n statusEl.innerHTML = \"\";\n publishBtn.style.display = \"none\";\n return;\n }\n\n var first = document.querySelector(\"[data-emdash-ref]\");\n if (!first) {\n statusEl.innerHTML = \"\";\n publishBtn.style.display = \"none\";\n return;\n }\n\n try {\n var ref = JSON.parse(first.getAttribute(\"data-emdash-ref\"));\n entryRef = ref;\n if (!ref.status) return;\n\n // Show admin link\n var adminLink = document.getElementById(\"emdash-tb-admin\");\n if (adminLink) {\n adminLink.href = \"/_emdash/admin/content/\" + encodeURIComponent(ref.collection) + \"/\" + encodeURIComponent(ref.id);\n adminLink.style.display = \"\";\n }\n\n if (ref.status === \"draft\") {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--draft\">Draft</span>';\n publishBtn.style.display = \"\";\n publishBtn.onclick = function() { publish(ref.collection, ref.id); };\n } else if (ref.status === \"published\" && ref.hasDraft) {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--pending\">Unpublished changes</span>';\n publishBtn.style.display = \"\";\n publishBtn.onclick = function() { publish(ref.collection, ref.id); };\n } else if (ref.status === \"published\") {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--published\">Published</span>';\n publishBtn.style.display = \"none\";\n }\n } catch (e) {\n // ignore parse errors\n }\n }\n\n // Publish action\n function publish(collection, id) {\n if (pendingSavePromise) {\n pendingSavePromise.then(function() { publish(collection, id); });\n return;\n }\n\n publishBtn.disabled = true;\n publishBtn.textContent = \"Publishing\\u2026\";\n\n ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id) + \"/publish\", {\n method: \"POST\",\n credentials: \"same-origin\",\n })\n .then(function(res) {\n if (res.ok) {\n if (document.startViewTransition) {\n document.startViewTransition(function() { location.reload(); });\n } else {\n location.reload();\n }\n } else {\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n console.error(\"Publish failed:\", res.status);\n }\n })\n .catch(function(err) {\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n console.error(\"Publish failed:\", err);\n });\n }\n\n // Edit mode toggle\n toggle.addEventListener(\"change\", function() {\n if (toggle.checked) {\n document.cookie = \"emdash-edit-mode=true;path=/;samesite=lax\";\n } else {\n document.cookie = \"emdash-edit-mode=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT\";\n }\n\n if (document.startViewTransition) {\n document.startViewTransition(function() { location.replace(location.href); });\n } else {\n location.replace(location.href);\n }\n });\n\n // --- Inline editing ---\n\n // Cached manifest (fetched once on first edit click)\n var manifestCache = null;\n var manifestPromise = null;\n\n function fetchManifest() {\n if (manifestCache) return Promise.resolve(manifestCache);\n if (manifestPromise) return manifestPromise;\n manifestPromise = ecFetch(\"/_emdash/api/manifest\", { credentials: \"same-origin\" })\n .then(function(r) { return r.json(); })\n .then(function(m) { manifestCache = m; return m; });\n return manifestPromise;\n }\n\n function getFieldKind(manifest, collection, field) {\n var col = manifest.collections && manifest.collections[collection];\n if (!col || !col.fields) return null;\n var f = col.fields[field];\n return f ? f.kind : null;\n }\n\n // Load manifest early so the first click can resolve field kinds without racing the event.\n if (isEditMode) {\n fetchManifest();\n }\n\n // Save a single field value\n function saveField(collection, id, field, value) {\n setSaveState(\"saving\");\n return ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id), {\n method: \"PUT\",\n credentials: \"same-origin\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: { [field]: value } }),\n })\n .then(function(res) {\n if (res.ok) {\n setSaveState(\"saved\");\n // A save creates/updates a draft — show unpublished changes\n showUnpublishedChanges(collection, id);\n } else {\n setSaveState(\"error\");\n console.error(\"Save failed:\", res.status);\n }\n })\n .catch(function(err) {\n setSaveState(\"error\");\n console.error(\"Save failed:\", err);\n });\n }\n\n function showUnpublishedChanges(collection, id) {\n statusEl.innerHTML = '<span class=\"emdash-tb-badge emdash-tb-badge--pending\">Unpublished changes</span>';\n publishBtn.style.display = \"\";\n publishBtn.disabled = false;\n publishBtn.textContent = \"Publish\";\n publishBtn.onclick = function() { publish(collection, id); };\n }\n\n // Plain text inline editing (contenteditable)\n var currentlyEditing = null;\n\n function startTextEdit(element, annotation) {\n if (currentlyEditing === element) return;\n if (currentlyEditing) endCurrentEdit();\n\n currentlyEditing = element;\n var originalText = element.textContent || \"\";\n\n element.setAttribute(\"data-emdash-editing\", \"\");\n element.contentEditable = \"plaintext-only\";\n element.focus();\n\n // Select all text\n var range = document.createRange();\n range.selectNodeContents(element);\n var sel = window.getSelection();\n sel.removeAllRanges();\n sel.addRange(range);\n\n // Track dirty state via input events\n function handleInput() {\n var current = (element.textContent || \"\").trim();\n if (current !== originalText.trim()) {\n setSaveState(\"unsaved\");\n } else {\n setSaveState(\"idle\");\n }\n }\n\n function handleBlur() {\n element.removeEventListener(\"blur\", handleBlur);\n element.removeEventListener(\"keydown\", handleKeydown);\n element.removeEventListener(\"input\", handleInput);\n element.contentEditable = \"false\";\n element.removeAttribute(\"data-emdash-editing\");\n currentlyEditing = null;\n\n var newValue = (element.textContent || \"\").trim();\n if (newValue !== originalText.trim()) {\n pendingSavePromise = saveField(annotation.collection, annotation.id, annotation.field, newValue).then(function() {\n pendingSavePromise = null;\n }, function() {\n pendingSavePromise = null;\n });\n } else {\n setSaveState(\"idle\");\n }\n }\n\n function handleKeydown(e) {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n element.blur();\n }\n if (e.key === \"Escape\") {\n element.textContent = originalText;\n setSaveState(\"idle\");\n element.blur();\n }\n }\n\n element.addEventListener(\"input\", handleInput);\n element.addEventListener(\"blur\", handleBlur);\n element.addEventListener(\"keydown\", handleKeydown);\n }\n\n function endCurrentEdit() {\n if (currentlyEditing) {\n currentlyEditing.blur();\n }\n }\n\n // Fallback: open admin\n function openAdmin(annotation) {\n var url = \"/_emdash/admin/content/\" + encodeURIComponent(annotation.collection) + \"/\" + encodeURIComponent(annotation.id);\n if (annotation.field) {\n url += \"?field=\" + encodeURIComponent(annotation.field);\n }\n window.open(url, \"emdash-admin\");\n }\n\n // --- Inline image editing ---\n var activeImagePopover = null;\n\n function closeImagePopover() {\n if (activeImagePopover) {\n activeImagePopover.backdrop.remove();\n activeImagePopover.popover.remove();\n if (activeImagePopover.escapeHandler) {\n document.removeEventListener(\"keydown\", activeImagePopover.escapeHandler);\n }\n activeImagePopover = null;\n }\n }\n\n function startImageEdit(element, annotation) {\n closeImagePopover();\n\n // Find the current image value by fetching the entry\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Find img element inside the annotated container (or the element itself if it's an img)\n var imgEl = element.tagName === \"IMG\" ? element : element.querySelector(\"img\");\n\n // Fetch current field value from the content API\n ecFetch(\"/_emdash/api/content/\" + encodeURIComponent(collection) + \"/\" + encodeURIComponent(id), {\n credentials: \"same-origin\"\n })\n .then(function(r) { return r.json(); })\n .then(function(entry) {\n var currentValue = entry.data && entry.data[field];\n showImagePopover(element, imgEl, annotation, currentValue);\n })\n .catch(function() {\n // If fetch fails, still show popover with what we can infer from DOM\n showImagePopover(element, imgEl, annotation, null);\n });\n }\n\n function showImagePopover(element, imgEl, annotation, currentValue) {\n closeImagePopover();\n\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Position near the element\n var rect = element.getBoundingClientRect();\n var viewportH = window.innerHeight;\n var viewportW = window.innerWidth;\n\n // Create backdrop for click-outside-to-close\n var backdrop = document.createElement(\"div\");\n backdrop.className = \"emdash-img-popover-backdrop\";\n backdrop.addEventListener(\"click\", function(e) {\n if (e.target === backdrop) closeImagePopover();\n });\n\n // Create popover\n var popover = document.createElement(\"div\");\n popover.className = \"emdash-img-popover\";\n\n var currentSrc = currentValue ? (currentValue.previewUrl || currentValue.src) : (imgEl ? imgEl.src : null);\n var currentAlt = currentValue ? (currentValue.alt || \"\") : (imgEl ? (imgEl.alt || \"\") : \"\");\n\n // Build popover HTML\n var html = '';\n html += '<div class=\"emdash-img-popover-header\">';\n html += ' <span class=\"emdash-img-popover-title\">Image</span>';\n html += ' <button class=\"emdash-img-popover-close\" data-action=\"close\">×</button>';\n html += '</div>';\n html += '<div class=\"emdash-img-popover-body\" id=\"emdash-img-main\">';\n\n if (currentSrc) {\n html += '<img class=\"emdash-img-preview\" src=\"' + escapeAttr(currentSrc) + '\" alt=\"\" />';\n } else {\n html += '<div class=\"emdash-img-empty\">No image selected</div>';\n }\n\n html += '<div class=\"emdash-img-field\">';\n html += ' <label for=\"emdash-img-alt\">Alt text</label>';\n html += ' <input type=\"text\" id=\"emdash-img-alt\" value=\"' + escapeAttr(currentAlt) + '\" placeholder=\"Describe the image\" />';\n html += '</div>';\n\n html += '<div class=\"emdash-img-actions\">';\n html += ' <button class=\"emdash-img-btn emdash-img-btn--primary\" data-action=\"browse\">Replace</button>';\n html += ' <label class=\"emdash-img-btn\" style=\"cursor:pointer\">';\n html += ' Upload';\n html += ' <input type=\"file\" accept=\"image/*\" id=\"emdash-img-upload\" style=\"display:none\" />';\n html += ' </label>';\n if (currentSrc) {\n html += ' <button class=\"emdash-img-btn emdash-img-btn--danger\" data-action=\"remove\">Remove</button>';\n }\n html += '</div>';\n html += '</div>';\n\n popover.innerHTML = html;\n\n backdrop.appendChild(popover);\n document.body.appendChild(backdrop);\n\n // Position the popover\n positionPopover(popover, rect, viewportW, viewportH);\n\n // Escape key handler\n function handleEscape(e) {\n if (e.key === \"Escape\") {\n closeImagePopover();\n document.removeEventListener(\"keydown\", handleEscape);\n }\n }\n document.addEventListener(\"keydown\", handleEscape);\n\n activeImagePopover = {\n backdrop: backdrop,\n popover: popover,\n annotation: annotation,\n currentValue: currentValue,\n element: element,\n imgEl: imgEl,\n escapeHandler: handleEscape\n };\n\n // Event handlers\n popover.querySelector('[data-action=\"close\"]').addEventListener(\"click\", closeImagePopover);\n\n popover.querySelector('[data-action=\"browse\"]').addEventListener(\"click\", function() {\n showMediaBrowser(popover, annotation, currentValue, element, imgEl);\n });\n\n var uploadInput = popover.querySelector(\"#emdash-img-upload\");\n uploadInput.addEventListener(\"change\", function(e) {\n var file = e.target.files && e.target.files[0];\n if (file) handleImageUpload(file, popover, annotation, element, imgEl);\n });\n\n var removeBtn = popover.querySelector('[data-action=\"remove\"]');\n if (removeBtn) {\n removeBtn.addEventListener(\"click\", function() {\n saveField(collection, id, field, null).then(function() {\n if (imgEl) {\n imgEl.style.display = \"none\";\n }\n closeImagePopover();\n });\n });\n }\n\n // Save alt text on change (debounced)\n var altInput = popover.querySelector(\"#emdash-img-alt\");\n var altTimer = null;\n altInput.addEventListener(\"input\", function() {\n clearTimeout(altTimer);\n altTimer = setTimeout(function() {\n var newAlt = altInput.value;\n if (currentValue) {\n var updated = Object.assign({}, currentValue, { alt: newAlt });\n saveField(collection, id, field, updated);\n if (imgEl) imgEl.alt = newAlt;\n }\n }, 500);\n });\n\n // Handle drag and drop on the popover body\n var body = popover.querySelector(\".emdash-img-popover-body\");\n body.addEventListener(\"dragover\", function(e) {\n e.preventDefault();\n body.classList.add(\"emdash-img-drop\");\n });\n body.addEventListener(\"dragleave\", function() {\n body.classList.remove(\"emdash-img-drop\");\n });\n body.addEventListener(\"drop\", function(e) {\n e.preventDefault();\n body.classList.remove(\"emdash-img-drop\");\n var file = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];\n if (file && file.type.startsWith(\"image/\")) {\n handleImageUpload(file, popover, annotation, element, imgEl);\n }\n });\n }\n\n function positionPopover(popover, targetRect, viewportW, viewportH) {\n var popoverW = 320;\n var gap = 8;\n\n // Try to place to the right of the element\n var left = targetRect.right + gap;\n var top = targetRect.top;\n\n // If it overflows right, place to the left\n if (left + popoverW > viewportW - 16) {\n left = targetRect.left - popoverW - gap;\n }\n // If it still overflows (narrow viewport), center below\n if (left < 16) {\n left = Math.max(16, (viewportW - popoverW) / 2);\n top = targetRect.bottom + gap;\n }\n // Clamp vertically\n if (top + 400 > viewportH - 80) { // 80 for toolbar\n top = Math.max(16, viewportH - 480);\n }\n if (top < 16) top = 16;\n\n popover.style.left = left + \"px\";\n popover.style.top = top + \"px\";\n }\n\n function escapeAttr(str) {\n return String(str || \"\").replace(/&/g, \"&\").replace(/\"/g, \""\").replace(/</g, \"<\").replace(/>/g, \">\");\n }\n\n function showMediaBrowser(popover, annotation, currentValue, element, imgEl) {\n var mainBody = popover.querySelector(\"#emdash-img-main\");\n if (mainBody) mainBody.style.display = \"none\";\n\n // Remove existing browser if any\n var existing = popover.querySelector(\".emdash-img-browser\");\n if (existing) existing.remove();\n\n var browser = document.createElement(\"div\");\n browser.className = \"emdash-img-browser\";\n\n browser.innerHTML = '<div class=\"emdash-img-browser-header\">' +\n '<span class=\"emdash-img-browser-title\">Media Library</span>' +\n '<button class=\"emdash-img-browser-back\">Back</button>' +\n '</div>' +\n '<div class=\"emdash-img-loading\">Loading\\u2026</div>';\n\n popover.appendChild(browser);\n\n browser.querySelector(\".emdash-img-browser-back\").addEventListener(\"click\", function() {\n browser.remove();\n if (mainBody) mainBody.style.display = \"\";\n });\n\n // Fetch media\n ecFetch(\"/_emdash/api/media?mimeType=image/&limit=30\", { credentials: \"same-origin\" })\n .then(function(r) { return r.json(); })\n .then(function(data) {\n var items = data.items || [];\n var loadingEl = browser.querySelector(\".emdash-img-loading\");\n if (loadingEl) loadingEl.remove();\n\n if (items.length === 0) {\n var empty = document.createElement(\"div\");\n empty.className = \"emdash-img-loading\";\n empty.textContent = \"No images found\";\n browser.appendChild(empty);\n return;\n }\n\n var grid = document.createElement(\"div\");\n grid.className = \"emdash-img-grid\";\n\n items.forEach(function(item) {\n var thumb = document.createElement(\"div\");\n thumb.className = \"emdash-img-grid-item\";\n if (currentValue && currentValue.id === item.id) {\n thumb.classList.add(\"emdash-img-grid-item--selected\");\n }\n var thumbUrl = item.url || item.previewUrl || (\"/_emdash/api/media/file/\" + item.storageKey);\n thumb.innerHTML = '<img src=\"' + escapeAttr(thumbUrl) + '\" alt=\"' + escapeAttr(item.alt || item.filename || \"\") + '\" loading=\"lazy\" />';\n\n thumb.addEventListener(\"click\", function() {\n selectMediaItem(item, annotation, element, imgEl);\n });\n\n grid.appendChild(thumb);\n });\n\n browser.appendChild(grid);\n })\n .catch(function(err) {\n var loadingEl = browser.querySelector(\".emdash-img-loading\");\n if (loadingEl) loadingEl.textContent = \"Failed to load media\";\n console.error(\"Media fetch error:\", err);\n });\n }\n\n function selectMediaItem(item, annotation, element, imgEl) {\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n var isLocal = !item.provider || item.provider === \"local\";\n var itemUrl = item.url || item.previewUrl || (\"/_emdash/api/media/file/\" + item.storageKey);\n\n var newValue = {\n id: item.id,\n provider: item.provider || \"local\",\n src: isLocal ? itemUrl : undefined,\n previewUrl: isLocal ? undefined : itemUrl,\n alt: item.alt || \"\",\n width: item.width,\n height: item.height,\n meta: item.meta\n };\n\n // Clean undefined fields\n Object.keys(newValue).forEach(function(k) {\n if (newValue[k] === undefined) delete newValue[k];\n });\n\n saveField(collection, id, field, newValue).then(function() {\n // Update the image in the DOM\n if (imgEl) {\n imgEl.src = itemUrl;\n imgEl.alt = item.alt || \"\";\n imgEl.style.display = \"\";\n }\n closeImagePopover();\n });\n }\n\n function handleImageUpload(file, popover, annotation, element, imgEl) {\n var collection = annotation.collection;\n var id = annotation.id;\n var field = annotation.field;\n\n // Show uploading state\n var mainBody = popover.querySelector(\"#emdash-img-main\");\n var browserEl = popover.querySelector(\".emdash-img-browser\");\n if (browserEl) browserEl.remove();\n if (mainBody) {\n mainBody.innerHTML = '<div class=\"emdash-img-uploading\">' +\n '<span>Uploading ' + escapeAttr(file.name) + '\\u2026</span>' +\n '</div>';\n mainBody.style.display = \"\";\n }\n\n // Detect dimensions before upload\n var dimPromise = new Promise(function(resolve) {\n if (!file.type.startsWith(\"image/\")) return resolve({});\n var img = new Image();\n img.onload = function() {\n resolve({ width: img.naturalWidth, height: img.naturalHeight });\n URL.revokeObjectURL(img.src);\n };\n img.onerror = function() {\n resolve({});\n URL.revokeObjectURL(img.src);\n };\n img.src = URL.createObjectURL(file);\n });\n\n dimPromise.then(function(dims) {\n // Generate a thumbnail for large images to avoid OOM in server-side\n // blurhash generation on memory-constrained runtimes (Workers).\n // Thumbnail fits within a 64x64 box (scale by max dimension) so that\n // extreme aspect ratios don't explode into a huge canvas client-side.\n var thumbPromise;\n if (dims.width && dims.height && dims.width * dims.height * 4 > 32 * 1024 * 1024) {\n thumbPromise = new Promise(function(resolve) {\n try {\n var maxDim = Math.max(dims.width, dims.height);\n var scale = Math.min(1, 64 / maxDim);\n var thumbW = Math.max(1, Math.round(dims.width * scale));\n var thumbH = Math.max(1, Math.round(dims.height * scale));\n var canvas = document.createElement(\"canvas\");\n canvas.width = thumbW;\n canvas.height = thumbH;\n var ctx = canvas.getContext(\"2d\");\n if (ctx) {\n var img = new Image();\n img.onload = function() {\n try {\n ctx.drawImage(img, 0, 0, thumbW, thumbH);\n canvas.toBlob(function(blob) {\n URL.revokeObjectURL(img.src);\n resolve(blob);\n }, \"image/png\");\n } catch (e) {\n URL.revokeObjectURL(img.src);\n resolve(null);\n }\n };\n img.onerror = function() {\n URL.revokeObjectURL(img.src);\n resolve(null);\n };\n img.src = URL.createObjectURL(file);\n } else {\n resolve(null);\n }\n } catch (e) {\n resolve(null);\n }\n });\n } else {\n thumbPromise = Promise.resolve(null);\n }\n\n return thumbPromise.then(function(thumbnail) {\n var formData = new FormData();\n formData.append(\"file\", file);\n if (dims.width) formData.append(\"width\", String(dims.width));\n if (dims.height) formData.append(\"height\", String(dims.height));\n if (thumbnail) formData.append(\"thumbnail\", thumbnail, \"thumb.png\");\n\n return ecFetch(\"/_emdash/api/media\", {\n method: \"POST\",\n credentials: \"same-origin\",\n body: formData\n });\n });\n })\n .then(function(r) { return r.json(); })\n .then(function(data) {\n if (!data.item) throw new Error(\"Upload failed\");\n var item = data.item;\n selectMediaItem(item, annotation, element, imgEl);\n })\n .catch(function(err) {\n console.error(\"Upload error:\", err);\n setSaveState(\"error\");\n closeImagePopover();\n });\n }\n\n // Click handler for edit mode\n if (isEditMode) {\n document.addEventListener(\"click\", function(e) {\n var target = e.target;\n\n // Don't intercept clicks on elements currently being edited\n if (target.hasAttribute && target.hasAttribute(\"data-emdash-editing\")) return;\n\n // Walk up to find annotated element\n while (target && target !== document.body) {\n if (target.hasAttribute && target.hasAttribute(\"data-emdash-editing\")) return;\n\n var ref = target.getAttribute && target.getAttribute(\"data-emdash-ref\");\n if (ref) {\n try {\n var annotation = JSON.parse(ref);\n\n // Entry-level annotation (no field) — keep walking for a field-level ancestor\n if (!annotation.field) {\n target = target.parentElement;\n continue;\n }\n\n function dispatchInline(kind) {\n closeImagePopover();\n // Portable Text is edited in-page by InlinePortableTextEditor — do not open admin\n if (kind === \"portableText\") {\n return;\n }\n e.preventDefault();\n e.stopPropagation();\n if (kind === \"string\" || kind === \"text\") {\n startTextEdit(target, annotation);\n } else if (kind === \"image\") {\n startImageEdit(target, annotation);\n } else {\n openAdmin(annotation);\n }\n }\n\n if (manifestCache) {\n dispatchInline(getFieldKind(manifestCache, annotation.collection, annotation.field));\n } else {\n fetchManifest().then(function(manifest) {\n dispatchInline(getFieldKind(manifest, annotation.collection, annotation.field));\n });\n }\n } catch (err) {\n console.error(\"Failed to parse emdash ref:\", err);\n }\n return;\n }\n target = target.parentElement;\n }\n }, true);\n }\n\n updateStatus();\n})();\n</script>\n`;\n}\n","/**\n * EmDash Request Context Middleware\n *\n * Sets up AsyncLocalStorage-based request context for query functions.\n * Skips ALS entirely for logged-out users with no CMS signals (fast path).\n *\n * Handles:\n * - Preview tokens: _preview query param with signed HMAC token\n * - Edit mode: emdash-edit-mode cookie (for visual editing)\n * - Toolbar injection: floating pill for authenticated editors\n */\n\nimport { defineMiddleware } from \"astro:middleware\";\n\nimport { verifyPreviewToken, parseContentId } from \"../../preview/tokens.js\";\nimport { runWithContext } from \"../../request-context.js\";\nimport { renderToolbar } from \"../../visual-editing/toolbar.js\";\n\n/**\n * Inject toolbar HTML into a response if it's an HTML page.\n * Returns the original response if not HTML.\n */\nasync function injectToolbar(response: Response, toolbarHtml: string): Promise<Response> {\n\tconst contentType = response.headers.get(\"content-type\");\n\tif (!contentType?.includes(\"text/html\")) return response;\n\n\tconst html = await response.text();\n\tif (!html.includes(\"</body>\")) return new Response(html, response);\n\n\tconst injected = html.replace(\"</body>\", `${toolbarHtml}</body>`);\n\treturn new Response(injected, {\n\t\tstatus: response.status,\n\t\theaders: response.headers,\n\t});\n}\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst { cookies, url } = context;\n\n\t// Skip /_emdash routes (admin has its own UI, no rendering context needed)\n\tif (url.pathname.startsWith(\"/_emdash\")) {\n\t\treturn next();\n\t}\n\n\t// Check for authenticated editor (role >= 30)\n\tconst { user } = context.locals;\n\tconst isEditor = !!user && user.role >= 30;\n\n\t// Playground mode: the playground middleware (from @emdash-cms/cloudflare) stashes\n\t// the per-session DO database on locals.__playgroundDb. We set it via ALS here\n\t// (same module instance as the loader) so getDb() picks it up correctly.\n\tconst playgroundDb = context.locals.__playgroundDb;\n\tif (playgroundDb) {\n\t\t// Check if playground user has toggled edit mode on\n\t\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\t\treturn runWithContext({ editMode: hasEditCookie, db: playgroundDb }, () => next());\n\t}\n\n\t// Fast path: check for CMS signals before doing any work\n\tconst hasEditCookie = cookies.get(\"emdash-edit-mode\")?.value === \"true\";\n\tconst hasPreviewToken = url.searchParams.has(\"_preview\");\n\n\t// No CMS signals and not an editor → skip everything (zero overhead)\n\tif (!hasEditCookie && !hasPreviewToken && !isEditor) {\n\t\treturn next();\n\t}\n\n\t// Determine edit mode: cookie AND authenticated editor\n\tconst editMode = hasEditCookie && isEditor;\n\n\t// Read locale from Astro's i18n routing\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Astro context includes currentLocale when i18n is configured\n\tconst locale = (context as { currentLocale?: string }).currentLocale;\n\n\t// Verify preview token if present\n\tlet preview: { collection: string; id: string } | undefined;\n\tif (hasPreviewToken) {\n\t\tconst secret = import.meta.env.EMDASH_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET || \"\";\n\n\t\tif (secret) {\n\t\t\tconst result = await verifyPreviewToken({ url, secret });\n\t\t\tif (result.valid) {\n\t\t\t\tconst { collection, id } = parseContentId(result.payload.cid);\n\t\t\t\tpreview = { collection, id };\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we have CMS signals, wrap in ALS context\n\tconst needsContext = hasEditCookie || hasPreviewToken;\n\n\tif (needsContext) {\n\t\treturn runWithContext({ editMode, preview, locale }, async () => {\n\t\t\tlet response = await next();\n\n\t\t\t// Preview responses must not be cached -- draft content could leak past token expiry.\n\t\t\t// Clone the response before modifying headers — the original may be immutable.\n\t\t\tif (preview) {\n\t\t\t\tresponse = new Response(response.body, response);\n\t\t\t\tresponse.headers.set(\"Cache-Control\", \"private, no-store\");\n\t\t\t}\n\n\t\t\t// Inject toolbar for authenticated editors\n\t\t\tif (isEditor) {\n\t\t\t\tconst toolbarHtml = renderToolbar({\n\t\t\t\t\teditMode,\n\t\t\t\t\tisPreview: !!preview,\n\t\t\t\t});\n\t\t\t\treturn injectToolbar(response, toolbarHtml);\n\t\t\t}\n\n\t\t\treturn response;\n\t\t});\n\t}\n\n\t// Editor without CMS signals — no ALS needed, but inject toolbar\n\tif (isEditor) {\n\t\tconst response = await next();\n\t\tconst toolbarHtml = renderToolbar({\n\t\t\teditMode: false,\n\t\t\tisPreview: false,\n\t\t});\n\t\treturn injectToolbar(response, toolbarHtml);\n\t}\n\n\treturn next();\n});\n\nexport default onRequest;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/astro/middleware.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/astro/middleware.ts"],"mappings":";;;;;;AAiLA;;;cAAa,SAAA,EAiRX,KAAA,CAjRoB,iBAAA"}
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import "../connection-B4zVnQIa.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { t as validateIdentifier } from "../validate-VPnKoIzW.mjs";
|
|
3
|
+
import { a as isSqlite } from "../dialect-helpers-DhTzaUxP.mjs";
|
|
4
|
+
import { r as runMigrations } from "../runner-Cd-_WyDo.mjs";
|
|
5
|
+
import { $ as sanitizeHeadersForSandbox, At as handleContentGet, Bt as handleContentUnschedule, Ct as handleContentCompare, Dt as handleContentDelete, Et as handleContentCreate, Ft as handleContentPublish, G as DEV_CONSOLE_EMAIL_PLUGIN_ID, Ht as validateRev, It as handleContentRestore, K as devConsoleEmailDeliver, Lt as handleContentSchedule, Mt as handleContentList, Nt as handleContentListTrashed, Ot as handleContentDiscardDraft, Pt as handleContentPermanentDelete, Q as extractRequestMeta, Rt as handleContentTranslations, St as hashString, Tt as handleContentCountTrashed, Vt as handleContentUpdate, W as PluginRouteRegistry, X as resolveExclusiveHooks, Y as createHookPipeline, Z as CronExecutor, _t as handleRevisionGet, et as definePlugin, ft as handleMediaCreate, gt as handleMediaUpdate, ht as handleMediaList, jt as handleContentGetIncludingTrashed, kt as handleContentDuplicate, mt as handleMediaGet, pt as handleMediaDelete, q as EmailPipeline, st as loadBundleFromR2, ut as PluginStateRepository, vt as handleRevisionList, wt as handleContentCountScheduled, yt as handleRevisionRestore, zt as handleContentUnpublish } from "../search-B5p9D36n.mjs";
|
|
6
|
+
import { r as RevisionRepository } from "../content-BsBoyj8G.mjs";
|
|
6
7
|
import "../base64-MBPo9ozB.mjs";
|
|
7
8
|
import "../types-CMMN0pNg.mjs";
|
|
8
9
|
import { t as MediaRepository } from "../media-DqHVh136.mjs";
|
|
9
|
-
import { f as OptionsRepository } from "../apply-
|
|
10
|
-
import
|
|
11
|
-
import "../
|
|
12
|
-
import "../
|
|
13
|
-
import {
|
|
14
|
-
import { i as
|
|
10
|
+
import { f as OptionsRepository } from "../apply-Cma_PiF6.mjs";
|
|
11
|
+
import "../redirect-7lGhLBNZ.mjs";
|
|
12
|
+
import "../byline-WuOq9MFJ.mjs";
|
|
13
|
+
import { n as normalizeMediaValue } from "../placeholder-DntBEQo7.mjs";
|
|
14
|
+
import { i as setI18nConfig } from "../config-DkxPrM9l.mjs";
|
|
15
|
+
import { i as FTSManager, n as SchemaRegistry } from "../registry-BgnP3ysR.mjs";
|
|
15
16
|
import { getRequestContext, runWithContext } from "../request-context.mjs";
|
|
16
|
-
import { n as getDb } from "../loader-
|
|
17
|
-
import { r as normalizeManifestRoute } from "../manifest-schema-
|
|
18
|
-
import "../query-
|
|
19
|
-
import "../tokens-
|
|
20
|
-
import "../bylines-
|
|
21
|
-
import "../load-
|
|
17
|
+
import { n as getDb } from "../loader-BYzwzORf.mjs";
|
|
18
|
+
import { r as normalizeManifestRoute } from "../manifest-schema-BsXINkQD.mjs";
|
|
19
|
+
import { a as invalidateUrlPatternCache } from "../query-B6Vu0d2i.mjs";
|
|
20
|
+
import "../tokens-DKHiCYCB.mjs";
|
|
21
|
+
import "../bylines-C_Wsnz4L.mjs";
|
|
22
|
+
import "../load-BhSSm-TS.mjs";
|
|
22
23
|
import "../index.mjs";
|
|
23
|
-
import { t as
|
|
24
|
+
import { n as VERSION, t as COMMIT } from "../version-DlTDRdpv.mjs";
|
|
25
|
+
import { t as getAuthMode } from "../mode-CyPLdO3C.mjs";
|
|
24
26
|
import { Kysely, sql } from "kysely";
|
|
25
27
|
import { defineMiddleware } from "astro:middleware";
|
|
26
28
|
import virtualConfig from "virtual:emdash/config";
|
|
@@ -118,10 +120,10 @@ async function runSystemCleanup(db, storage) {
|
|
|
118
120
|
*/
|
|
119
121
|
async function pruneExcessiveRevisions(db) {
|
|
120
122
|
const entries = await sql`
|
|
121
|
-
SELECT collection, entry_id
|
|
123
|
+
SELECT collection, entry_id
|
|
122
124
|
FROM revisions
|
|
123
125
|
GROUP BY collection, entry_id
|
|
124
|
-
HAVING
|
|
126
|
+
HAVING COUNT(*) > ${REVISION_PRUNE_THRESHOLD}
|
|
125
127
|
`.execute(db);
|
|
126
128
|
if (entries.rows.length === 0) return 0;
|
|
127
129
|
const revisionRepo = new RevisionRepository(db);
|
|
@@ -288,6 +290,7 @@ const VALID_LINK_REL = new Set([
|
|
|
288
290
|
"alternate",
|
|
289
291
|
"author",
|
|
290
292
|
"license",
|
|
293
|
+
"nlweb",
|
|
291
294
|
"site.standard.document"
|
|
292
295
|
]);
|
|
293
296
|
/**
|
|
@@ -722,9 +725,9 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
722
725
|
}
|
|
723
726
|
})();
|
|
724
727
|
if (collectionCount.count === 0 && !setupDone) {
|
|
725
|
-
const { applySeed } = await import("../apply-
|
|
726
|
-
const { loadSeed } = await import("../load-
|
|
727
|
-
const { validateSeed } = await import("../validate-
|
|
728
|
+
const { applySeed } = await import("../apply-Cma_PiF6.mjs").then((n) => n.n);
|
|
729
|
+
const { loadSeed } = await import("../load-BhSSm-TS.mjs").then((n) => n.r);
|
|
730
|
+
const { validateSeed } = await import("../validate-DuZDIxfy.mjs").then((n) => n.n);
|
|
728
731
|
const seed = await loadSeed();
|
|
729
732
|
if (validateSeed(seed).valid) {
|
|
730
733
|
await applySeed(db, seed, { onConflict: "skip" });
|
|
@@ -971,7 +974,8 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
971
974
|
locales: i18nConfig.locales
|
|
972
975
|
} : void 0;
|
|
973
976
|
return {
|
|
974
|
-
version:
|
|
977
|
+
version: VERSION,
|
|
978
|
+
commit: COMMIT,
|
|
975
979
|
hash: manifestHash,
|
|
976
980
|
collections: manifestCollections,
|
|
977
981
|
plugins: manifestPlugins,
|
|
@@ -982,10 +986,12 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
982
986
|
};
|
|
983
987
|
}
|
|
984
988
|
/**
|
|
985
|
-
* Invalidate
|
|
986
|
-
*
|
|
989
|
+
* Invalidate cached data derived from the manifest/schema.
|
|
990
|
+
* Called when collections are created, updated, or deleted.
|
|
987
991
|
*/
|
|
988
|
-
invalidateManifest() {
|
|
992
|
+
invalidateManifest() {
|
|
993
|
+
invalidateUrlPatternCache();
|
|
994
|
+
}
|
|
989
995
|
async handleContentList(collection, params) {
|
|
990
996
|
return handleContentList(this.db, collection, params);
|
|
991
997
|
}
|
|
@@ -1010,7 +1016,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1010
1016
|
return result;
|
|
1011
1017
|
}
|
|
1012
1018
|
async handleContentUpdate(collection, id, body) {
|
|
1013
|
-
const { ContentRepository } = await import("../content-
|
|
1019
|
+
const { ContentRepository } = await import("../content-BsBoyj8G.mjs").then((n) => n.n);
|
|
1014
1020
|
const repo = new ContentRepository(this.db);
|
|
1015
1021
|
const resolvedItem = await repo.findByIdOrSlug(collection, id);
|
|
1016
1022
|
const resolvedId = resolvedItem?.id ?? id;
|
|
@@ -1061,6 +1067,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1061
1067
|
data: mergedData,
|
|
1062
1068
|
authorId: bodyWithoutRev.authorId ?? void 0
|
|
1063
1069
|
});
|
|
1070
|
+
validateIdentifier(collection, "collection");
|
|
1064
1071
|
const tableName = `ec_${collection}`;
|
|
1065
1072
|
await sql`
|
|
1066
1073
|
UPDATE ${sql.ref(tableName)}
|
|
@@ -1102,7 +1109,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1102
1109
|
}
|
|
1103
1110
|
};
|
|
1104
1111
|
const result = await handleContentDelete(this.db, collection, id);
|
|
1105
|
-
if (result.success) this.runAfterDeleteHooks(id, collection);
|
|
1112
|
+
if (result.success) this.runAfterDeleteHooks(id, collection, false);
|
|
1106
1113
|
return result;
|
|
1107
1114
|
}
|
|
1108
1115
|
async handleContentListTrashed(collection, params = {}) {
|
|
@@ -1112,7 +1119,9 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1112
1119
|
return handleContentRestore(this.db, collection, id);
|
|
1113
1120
|
}
|
|
1114
1121
|
async handleContentPermanentDelete(collection, id) {
|
|
1115
|
-
|
|
1122
|
+
const result = await handleContentPermanentDelete(this.db, collection, id);
|
|
1123
|
+
if (result.success) this.runAfterDeleteHooks(id, collection, true);
|
|
1124
|
+
return result;
|
|
1116
1125
|
}
|
|
1117
1126
|
async handleContentCountTrashed(collection) {
|
|
1118
1127
|
return handleContentCountTrashed(this.db, collection);
|
|
@@ -1340,14 +1349,15 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1340
1349
|
}).catch((err) => console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err));
|
|
1341
1350
|
}
|
|
1342
1351
|
}
|
|
1343
|
-
runAfterDeleteHooks(id, collection) {
|
|
1344
|
-
if (this.hooks.hasHooks("content:afterDelete")) this.hooks.runContentAfterDelete(id, collection).catch((err) => console.error("EmDash afterDelete hook error:", err));
|
|
1352
|
+
runAfterDeleteHooks(id, collection, permanent) {
|
|
1353
|
+
if (this.hooks.hasHooks("content:afterDelete")) this.hooks.runContentAfterDelete(id, collection, permanent).catch((err) => console.error("EmDash afterDelete hook error:", err));
|
|
1345
1354
|
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
1346
1355
|
const [pluginId] = pluginKey.split(":");
|
|
1347
1356
|
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
1348
1357
|
plugin.invokeHook("content:afterDelete", {
|
|
1349
1358
|
id,
|
|
1350
|
-
collection
|
|
1359
|
+
collection,
|
|
1360
|
+
permanent
|
|
1351
1361
|
}).catch((err) => console.error(`EmDash: Sandboxed plugin ${pluginId} afterDelete error:`, err));
|
|
1352
1362
|
}
|
|
1353
1363
|
}
|
|
@@ -1552,10 +1562,12 @@ async function getRuntime(config) {
|
|
|
1552
1562
|
* Admin routes get additional headers (strict CSP) from auth middleware.
|
|
1553
1563
|
*/
|
|
1554
1564
|
function setBaselineSecurityHeaders(response) {
|
|
1555
|
-
response.
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1565
|
+
const res = new Response(response.body, response);
|
|
1566
|
+
res.headers.set("X-Content-Type-Options", "nosniff");
|
|
1567
|
+
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1568
|
+
res.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()");
|
|
1569
|
+
if (!res.headers.has("Content-Security-Policy")) res.headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
1570
|
+
return res;
|
|
1559
1571
|
}
|
|
1560
1572
|
/** Public routes that require the runtime (sitemap, robots.txt, etc.) */
|
|
1561
1573
|
const PUBLIC_RUNTIME_ROUTES = new Set(["/sitemap.xml", "/robots.txt"]);
|
|
@@ -1571,7 +1583,7 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
1571
1583
|
if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
|
|
1572
1584
|
if (!(context.isPrerendered ? null : await context.session?.get("user")) && !playgroundDb) {
|
|
1573
1585
|
if (!setupVerified) try {
|
|
1574
|
-
const { getDb } = await import("../loader-
|
|
1586
|
+
const { getDb } = await import("../loader-BYzwzORf.mjs").then((n) => n.r);
|
|
1575
1587
|
await (await getDb()).selectFrom("_emdash_migrations").selectAll().limit(1).execute();
|
|
1576
1588
|
setupVerified = true;
|
|
1577
1589
|
} catch {
|
|
@@ -1586,9 +1598,7 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
1586
1598
|
collectPageFragments: runtime.collectPageFragments.bind(runtime)
|
|
1587
1599
|
};
|
|
1588
1600
|
} catch {}
|
|
1589
|
-
|
|
1590
|
-
setBaselineSecurityHeaders(response);
|
|
1591
|
-
return response;
|
|
1601
|
+
return setBaselineSecurityHeaders(await next());
|
|
1592
1602
|
}
|
|
1593
1603
|
}
|
|
1594
1604
|
const config = getConfig();
|
|
@@ -1668,8 +1678,7 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
1668
1678
|
editMode: false,
|
|
1669
1679
|
db: new Kysely({ dialect: createSessionDialect(session) })
|
|
1670
1680
|
}, async () => {
|
|
1671
|
-
const response = await next();
|
|
1672
|
-
setBaselineSecurityHeaders(response);
|
|
1681
|
+
const response = setBaselineSecurityHeaders(await next());
|
|
1673
1682
|
if (isAuthenticated && session && typeof session === "object" && "getBookmark" in session) {
|
|
1674
1683
|
const newBookmark = session.getBookmark.call(session);
|
|
1675
1684
|
if (newBookmark) response.headers.append("Set-Cookie", `${cookieName}=${newBookmark}; Path=/; HttpOnly; SameSite=Lax; Secure`);
|
|
@@ -1678,9 +1687,7 @@ const onRequest = defineMiddleware(async (context, next) => {
|
|
|
1678
1687
|
});
|
|
1679
1688
|
}
|
|
1680
1689
|
}
|
|
1681
|
-
|
|
1682
|
-
setBaselineSecurityHeaders(response);
|
|
1683
|
-
return response;
|
|
1690
|
+
return setBaselineSecurityHeaders(await next());
|
|
1684
1691
|
};
|
|
1685
1692
|
if (playgroundDb) return runWithContext({
|
|
1686
1693
|
editMode: context.cookies.get("emdash-edit-mode")?.value === "true",
|