veryfront 0.1.31 → 0.1.32
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/esm/deno.js +1 -1
- package/esm/src/html/dev-scripts.d.ts +4 -0
- package/esm/src/html/dev-scripts.d.ts.map +1 -1
- package/esm/src/html/dev-scripts.js +2 -0
- package/esm/src/html/html-injection.d.ts +4 -0
- package/esm/src/html/html-injection.d.ts.map +1 -1
- package/esm/src/html/html-injection.js +2 -0
- package/esm/src/server/handlers/preview/markdown-html-generator.d.ts +4 -0
- package/esm/src/server/handlers/preview/markdown-html-generator.d.ts.map +1 -1
- package/esm/src/server/handlers/preview/markdown-html-generator.js +13 -4
- package/esm/src/server/handlers/preview/markdown-preview.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/preview/markdown-preview.handler.js +2 -0
- package/esm/src/server/handlers/studio/endpoints.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/studio/endpoints.handler.js +3 -1
- package/esm/src/studio/bridge-template.d.ts +2 -0
- package/esm/src/studio/bridge-template.d.ts.map +1 -1
- package/esm/src/studio/bridge-template.js +174 -97
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/src/html/dev-scripts.ts +6 -0
- package/src/src/html/html-injection.ts +6 -0
- package/src/src/server/handlers/preview/markdown-html-generator.ts +25 -4
- package/src/src/server/handlers/preview/markdown-preview.handler.ts +2 -0
- package/src/src/server/handlers/studio/endpoints.handler.ts +3 -1
- package/src/src/studio/bridge-template.ts +176 -97
package/esm/deno.js
CHANGED
|
@@ -8,6 +8,10 @@ export interface StudioScriptOptions {
|
|
|
8
8
|
nonce?: string;
|
|
9
9
|
/** Hash of source code for sync detection with Navigator tree */
|
|
10
10
|
sourceHash?: string;
|
|
11
|
+
/** WebSocket URL for direct Yjs connection from the bridge */
|
|
12
|
+
wsUrl?: string;
|
|
13
|
+
/** Yjs document GUID for the bridge to join the same room */
|
|
14
|
+
yjsGuid?: string;
|
|
11
15
|
}
|
|
12
16
|
export declare function getStudioScripts(options: StudioScriptOptions): string;
|
|
13
17
|
//# sourceMappingURL=dev-scripts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-scripts.d.ts","sourceRoot":"","sources":["../../../src/src/html/dev-scripts.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CA+BnD;AAMD,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAMvE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAOnE;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"dev-scripts.d.ts","sourceRoot":"","sources":["../../../src/src/html/dev-scripts.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CA+BnD;AAMD,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAMvE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAOnE;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAiBrE"}
|
|
@@ -51,6 +51,8 @@ export function getStudioScripts(options) {
|
|
|
51
51
|
projectId: options.projectId,
|
|
52
52
|
pageId: options.pageId,
|
|
53
53
|
...(options.pagePath ? { pagePath: options.pagePath } : {}),
|
|
54
|
+
...(options.wsUrl ? { wsUrl: options.wsUrl } : {}),
|
|
55
|
+
...(options.yjsGuid ? { yjsGuid: options.yjsGuid } : {}),
|
|
54
56
|
}).toString();
|
|
55
57
|
const sourceHashScript = options.sourceHash
|
|
56
58
|
? `<script${nonceAttr}>window.__VERYFRONT_SOURCE_HASH__="${options.sourceHash}";</script>\n `
|
|
@@ -15,6 +15,10 @@ export interface InjectHTMLContentOptions {
|
|
|
15
15
|
pageId?: string;
|
|
16
16
|
/** CSP nonce */
|
|
17
17
|
nonce?: string;
|
|
18
|
+
/** WebSocket URL for direct Yjs connection from the bridge */
|
|
19
|
+
wsUrl?: string;
|
|
20
|
+
/** Yjs document GUID for the bridge to join the same room */
|
|
21
|
+
yjsGuid?: string;
|
|
18
22
|
}
|
|
19
23
|
export declare function injectHTMLContent(template: string, content: string, metadata: HTMLMetadata, options: InjectHTMLContentOptions): string;
|
|
20
24
|
//# sourceMappingURL=html-injection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-injection.d.ts","sourceRoot":"","sources":["../../../src/src/html/html-injection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAS/D,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"html-injection.d.ts","sourceRoot":"","sources":["../../../src/src/html/html-injection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAS/D,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,wBAAwB,GAChC,MAAM,CA4ER"}
|
|
@@ -56,6 +56,8 @@ export function injectHTMLContent(template, content, metadata, options) {
|
|
|
56
56
|
projectId: options.projectId ?? options.slug,
|
|
57
57
|
pageId: options.pageId ?? options.slug,
|
|
58
58
|
nonce: options.nonce,
|
|
59
|
+
wsUrl: options.wsUrl,
|
|
60
|
+
yjsGuid: options.yjsGuid,
|
|
59
61
|
});
|
|
60
62
|
html = html.replace(/<\/body>/i, `${studioScripts}</body>`);
|
|
61
63
|
}
|
|
@@ -24,6 +24,10 @@ export interface MarkdownHtmlOptions {
|
|
|
24
24
|
projectId: string;
|
|
25
25
|
/** File path of the markdown file. */
|
|
26
26
|
filePath: string;
|
|
27
|
+
/** Branch ID for Yjs room GUID computation. */
|
|
28
|
+
branchId?: string | null;
|
|
29
|
+
/** Request host for computing the WebSocket URL. */
|
|
30
|
+
requestHost?: string;
|
|
27
31
|
}
|
|
28
32
|
/**
|
|
29
33
|
* Generate a complete HTML document for markdown preview rendering.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-html-generator.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/preview/markdown-html-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAMrD,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC;IACzB,gDAAgD;IAChD,GAAG,EAAE,GAAG,CAAC;IACT,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown-html-generator.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/preview/markdown-html-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAMrD,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC;IACzB,gDAAgD;IAChD,GAAG,EAAE,GAAG,CAAC;IACT,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAgED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CA2FzE"}
|
|
@@ -25,19 +25,28 @@ function detectTheme(req, url) {
|
|
|
25
25
|
* Injected when embedded in Studio (`studio_embed=true`) or for standalone
|
|
26
26
|
* markdown/MDX pages so the edit button and editor features are available.
|
|
27
27
|
*/
|
|
28
|
-
function buildStudioScript(url, projectId, filePath) {
|
|
28
|
+
function buildStudioScript(url, projectId, filePath, branchId, requestHost) {
|
|
29
29
|
const studioEmbed = url.searchParams.get("studio_embed") === "true";
|
|
30
30
|
const isMarkdown = /\.mdx?$/i.test(filePath);
|
|
31
31
|
if (!studioEmbed && !isMarkdown)
|
|
32
32
|
return "";
|
|
33
|
-
const
|
|
33
|
+
const rawQueryProjectId = url.searchParams.get("vf_project_id")?.trim() || "";
|
|
34
|
+
// Validate query param to prevent path traversal in WebSocket URL
|
|
35
|
+
const queryProjectId = /^[a-zA-Z0-9_-]+$/.test(rawQueryProjectId) ? rawQueryProjectId : "";
|
|
34
36
|
const queryFileId = url.searchParams.get("vf_file_id")?.trim() || "";
|
|
35
37
|
const canonicalProjectId = queryProjectId || projectId;
|
|
36
38
|
const canonicalPageId = queryFileId || filePath;
|
|
39
|
+
// Compute Yjs connection config for the bridge to self-connect
|
|
40
|
+
const wsProtocol = url.protocol === "https:" ? "wss" : "ws";
|
|
41
|
+
const host = requestHost || url.host;
|
|
42
|
+
const wsUrl = `${wsProtocol}://${host}/api/ws/${canonicalProjectId}/yjs`;
|
|
43
|
+
const yjsGuid = branchId ? `${canonicalProjectId}:${branchId}` : canonicalProjectId;
|
|
37
44
|
return `<script>${generateStudioBridgeScript({
|
|
38
45
|
projectId: canonicalProjectId,
|
|
39
46
|
pageId: canonicalPageId,
|
|
40
47
|
pagePath: filePath,
|
|
48
|
+
wsUrl,
|
|
49
|
+
yjsGuid,
|
|
41
50
|
})}</script>`;
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
@@ -48,9 +57,9 @@ function buildStudioScript(url, projectId, filePath) {
|
|
|
48
57
|
* studio bridge integration.
|
|
49
58
|
*/
|
|
50
59
|
export function generateMarkdownHtml(options) {
|
|
51
|
-
const { rawHtml, title, description, request, url, projectId, filePath } = options;
|
|
60
|
+
const { rawHtml, title, description, request, url, projectId, filePath, branchId, requestHost } = options;
|
|
52
61
|
const theme = detectTheme(request, url);
|
|
53
|
-
const studioScript = buildStudioScript(url, projectId, filePath);
|
|
62
|
+
const studioScript = buildStudioScript(url, projectId, filePath, branchId, requestHost);
|
|
54
63
|
const themeAttrs = theme ? ` data-theme="${theme}" style="color-scheme: ${theme};"` : "";
|
|
55
64
|
return `<!DOCTYPE html>
|
|
56
65
|
<html lang="en"${themeAttrs}>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-preview.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/preview/markdown-preview.handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAmB,aAAa,EAAE,MAAM,aAAa,CAAC;AAgBnG,qBAAa,sBAAuB,SAAQ,WAAW;IACrD,QAAQ,EAAE,eAAe,CAKvB;IAEI,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;YAiEjE,cAAc;
|
|
1
|
+
{"version":3,"file":"markdown-preview.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/preview/markdown-preview.handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAmB,aAAa,EAAE,MAAM,aAAa,CAAC;AAgBnG,qBAAa,sBAAuB,SAAQ,WAAW;IACrD,QAAQ,EAAE,eAAe,CAKvB;IAEI,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;YAiEjE,cAAc;CAkF7B"}
|
|
@@ -108,6 +108,8 @@ export class MarkdownPreviewHandler extends BaseHandler {
|
|
|
108
108
|
url,
|
|
109
109
|
projectId: ctx.projectSlug || ctx.projectId || "markdown-preview",
|
|
110
110
|
filePath,
|
|
111
|
+
branchId: ctx.parsedDomain?.branch ?? null,
|
|
112
|
+
requestHost: url.host,
|
|
111
113
|
});
|
|
112
114
|
const responseBuilder = this.createResponseBuilder(ctx)
|
|
113
115
|
.withCache("no-cache")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoints.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/studio/endpoints.handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EAEf,aAAa,EACd,MAAM,aAAa,CAAC;AAIrB,qBAAa,sBAAuB,SAAQ,WAAW;IACrD,QAAQ,EAAE,eAAe,CAKvB;IAEF,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"endpoints.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/studio/endpoints.handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EAEf,aAAa,EACd,MAAM,aAAa,CAAC;AAIrB,qBAAa,sBAAuB,SAAQ,WAAW;IACrD,QAAQ,EAAE,eAAe,CAKvB;IAEF,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CAuB1E"}
|
|
@@ -20,7 +20,9 @@ export class StudioEndpointsHandler extends BaseHandler {
|
|
|
20
20
|
const projectId = url.searchParams.get("projectId") ?? "";
|
|
21
21
|
const pageId = url.searchParams.get("pageId") ?? "";
|
|
22
22
|
const pagePath = url.searchParams.get("pagePath") ?? undefined;
|
|
23
|
-
const
|
|
23
|
+
const wsUrl = url.searchParams.get("wsUrl") ?? undefined;
|
|
24
|
+
const yjsGuid = url.searchParams.get("yjsGuid") ?? undefined;
|
|
25
|
+
const script = generateStudioBridgeScript({ projectId, pageId, pagePath, wsUrl, yjsGuid });
|
|
24
26
|
const response = builder.withCache("no-cache").javascript(script, HTTP_OK);
|
|
25
27
|
return Promise.resolve(this.respond(response));
|
|
26
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-template.d.ts","sourceRoot":"","sources":["../../../src/src/studio/bridge-template.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"bridge-template.d.ts","sourceRoot":"","sources":["../../../src/src/studio/bridge-template.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAsvI/E"}
|
|
@@ -5,6 +5,8 @@ export function generateStudioBridgeScript(options) {
|
|
|
5
5
|
const PROJECT_ID = ${JSON.stringify(options.projectId)};
|
|
6
6
|
const PAGE_ID = ${JSON.stringify(options.pageId)};
|
|
7
7
|
const PAGE_PATH = ${JSON.stringify(options.pagePath ?? options.pageId)};
|
|
8
|
+
const WS_URL = ${JSON.stringify(options.wsUrl ?? "")};
|
|
9
|
+
const YJS_GUID = ${JSON.stringify(options.yjsGuid ?? "")};
|
|
8
10
|
const DEBUG_SKIP_INIT = ${options.debugSkipInit ? "true" : "false"};
|
|
9
11
|
const DEBUG_EXPOSE_INTERNALS = ${options.debugExposeInternals ? "true" : "false"};
|
|
10
12
|
|
|
@@ -75,6 +77,8 @@ export function generateStudioBridgeScript(options) {
|
|
|
75
77
|
let markdownYText = null;
|
|
76
78
|
let markdownYjsConnected = false;
|
|
77
79
|
let markdownYjsSetupId = 0;
|
|
80
|
+
let markdownYjsY = null;
|
|
81
|
+
let markdownPendingSelection = null;
|
|
78
82
|
const LEXICAL_YJS_ORIGIN = 'lexical-yjs-binding';
|
|
79
83
|
|
|
80
84
|
const MARKDOWN_SLASH_COMMANDS = [
|
|
@@ -1466,7 +1470,7 @@ export function generateStudioBridgeScript(options) {
|
|
|
1466
1470
|
|
|
1467
1471
|
Promise.all([
|
|
1468
1472
|
import('https://esm.sh/yjs@13.6.28?target=es2022'),
|
|
1469
|
-
import('https://esm.sh/y-websocket@2.1.0?target=es2022')
|
|
1473
|
+
import('https://esm.sh/y-websocket@2.1.0?deps=yjs@13.6.28&target=es2022')
|
|
1470
1474
|
]).then(function(modules) {
|
|
1471
1475
|
// Abort if edit mode was closed while imports were loading
|
|
1472
1476
|
if (setupId !== markdownYjsSetupId) {
|
|
@@ -1475,11 +1479,13 @@ export function generateStudioBridgeScript(options) {
|
|
|
1475
1479
|
|
|
1476
1480
|
var Y = modules[0];
|
|
1477
1481
|
var WebsocketProvider = modules[1].WebsocketProvider;
|
|
1482
|
+
markdownYjsY = Y;
|
|
1478
1483
|
|
|
1479
1484
|
var doc = new Y.Doc({ guid: config.guid });
|
|
1485
|
+
// Cookie auth: authToken cookie on .veryfront.com is sent automatically
|
|
1486
|
+
// with the WebSocket upgrade request. No explicit token param needed.
|
|
1480
1487
|
var provider = new WebsocketProvider(config.wsUrl, config.guid, doc, {
|
|
1481
|
-
resyncInterval: -1
|
|
1482
|
-
params: { token: config.authToken }
|
|
1488
|
+
resyncInterval: -1
|
|
1483
1489
|
});
|
|
1484
1490
|
|
|
1485
1491
|
var ytext = doc.getText(config.fileId);
|
|
@@ -1504,6 +1510,93 @@ export function generateStudioBridgeScript(options) {
|
|
|
1504
1510
|
}
|
|
1505
1511
|
});
|
|
1506
1512
|
|
|
1513
|
+
// Extract user identity from authToken JWT cookie for presence
|
|
1514
|
+
var presenceUser = { id: 'preview-' + Math.random().toString(36).slice(2), name: 'Preview' };
|
|
1515
|
+
try {
|
|
1516
|
+
var cookieMatch = document.cookie.match(/authToken=([^;]+)/);
|
|
1517
|
+
if (cookieMatch) {
|
|
1518
|
+
var parts = cookieMatch[1].split('.');
|
|
1519
|
+
if (parts.length === 3) {
|
|
1520
|
+
var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
1521
|
+
if (payload.userId) {
|
|
1522
|
+
presenceUser.id = payload.userId;
|
|
1523
|
+
}
|
|
1524
|
+
if (payload.email) {
|
|
1525
|
+
var local = payload.email.split('@')[0] || '';
|
|
1526
|
+
if (local.includes('.') || local.includes('_')) {
|
|
1527
|
+
presenceUser.name = local.split(/[._]/).map(function(p) { return p.charAt(0).toUpperCase() + p.slice(1); }).join(' ');
|
|
1528
|
+
} else {
|
|
1529
|
+
presenceUser.name = local.charAt(0).toUpperCase() + local.slice(1);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
} catch (e) {
|
|
1535
|
+
// Fall back to defaults on any parse error
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Set local user on awareness for presence
|
|
1539
|
+
provider.awareness.setLocalStateField('user', {
|
|
1540
|
+
id: presenceUser.id,
|
|
1541
|
+
name: presenceUser.name,
|
|
1542
|
+
color: '#10b981'
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
// Observe awareness for remote presence and selection changes
|
|
1546
|
+
function syncAwareness() {
|
|
1547
|
+
var states = Array.from(provider.awareness.getStates().entries());
|
|
1548
|
+
|
|
1549
|
+
// Sync presence users
|
|
1550
|
+
var users = [];
|
|
1551
|
+
for (var i = 0; i < states.length; i++) {
|
|
1552
|
+
var clientId = states[i][0];
|
|
1553
|
+
var state = states[i][1];
|
|
1554
|
+
var user = state.user;
|
|
1555
|
+
if (!user || typeof user.name !== 'string') {
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
users.push({
|
|
1559
|
+
id: user.id || String(clientId),
|
|
1560
|
+
name: user.name,
|
|
1561
|
+
color: user.color || '#6b7280',
|
|
1562
|
+
isCurrentUser: clientId === provider.awareness.clientID,
|
|
1563
|
+
isAgent: user.isAgent || false
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
setMarkdownPresence(users);
|
|
1567
|
+
|
|
1568
|
+
// Sync remote selections
|
|
1569
|
+
var selections = [];
|
|
1570
|
+
for (var j = 0; j < states.length; j++) {
|
|
1571
|
+
var cId = states[j][0];
|
|
1572
|
+
var st = states[j][1];
|
|
1573
|
+
var u = st.user;
|
|
1574
|
+
var ranges = st.selection;
|
|
1575
|
+
if (!u || !Array.isArray(ranges) || ranges.length === 0) {
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
for (var k = 0; k < ranges.length; k++) {
|
|
1579
|
+
var range = ranges[k];
|
|
1580
|
+
var anchorPos = Y.createAbsolutePositionFromRelativePosition(range.anchor, doc);
|
|
1581
|
+
var markerPos = Y.createAbsolutePositionFromRelativePosition(range.marker, doc);
|
|
1582
|
+
if (!anchorPos || !markerPos || anchorPos.type !== ytext || markerPos.type !== ytext) {
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
selections.push({
|
|
1586
|
+
id: u.id || String(cId),
|
|
1587
|
+
name: u.name || 'Anonymous',
|
|
1588
|
+
color: u.color || '#6b7280',
|
|
1589
|
+
isCurrentUser: cId === provider.awareness.clientID,
|
|
1590
|
+
start: Math.min(anchorPos.index, markerPos.index),
|
|
1591
|
+
end: Math.max(anchorPos.index, markerPos.index)
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
setMarkdownSelections(selections);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
provider.awareness.on('change', syncAwareness);
|
|
1599
|
+
|
|
1507
1600
|
provider.on('sync', function(synced) {
|
|
1508
1601
|
if (synced && !markdownYjsConnected) {
|
|
1509
1602
|
markdownYjsConnected = true;
|
|
@@ -1517,6 +1610,18 @@ export function generateStudioBridgeScript(options) {
|
|
|
1517
1610
|
applyMarkdownContent(ytextContent);
|
|
1518
1611
|
}
|
|
1519
1612
|
|
|
1613
|
+
// Replay any selection queued before Yjs was ready
|
|
1614
|
+
if (markdownPendingSelection) {
|
|
1615
|
+
var ps = markdownPendingSelection;
|
|
1616
|
+
markdownPendingSelection = null;
|
|
1617
|
+
var cs = Math.max(0, Math.min(ytext.length, ps.start));
|
|
1618
|
+
var ce = Math.max(0, Math.min(ytext.length, ps.end));
|
|
1619
|
+
provider.awareness.setLocalStateField('selection', [{
|
|
1620
|
+
anchor: Y.createRelativePositionFromTypeIndex(ytext, cs),
|
|
1621
|
+
marker: Y.createRelativePositionFromTypeIndex(ytext, ce)
|
|
1622
|
+
}]);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1520
1625
|
// Observe Y.Text for remote changes (from other users / Monaco)
|
|
1521
1626
|
ytext.observe(function(event) {
|
|
1522
1627
|
if (event.transaction.origin === LEXICAL_YJS_ORIGIN) {
|
|
@@ -1529,6 +1634,9 @@ export function generateStudioBridgeScript(options) {
|
|
|
1529
1634
|
applyMarkdownContent(fullContent);
|
|
1530
1635
|
});
|
|
1531
1636
|
|
|
1637
|
+
// Initial awareness sync after Yjs is connected
|
|
1638
|
+
syncAwareness();
|
|
1639
|
+
|
|
1532
1640
|
console.debug('[StudioBridge] Yjs synced, bound to Y.Text for fileId:', config.fileId);
|
|
1533
1641
|
}
|
|
1534
1642
|
});
|
|
@@ -1550,6 +1658,7 @@ export function generateStudioBridgeScript(options) {
|
|
|
1550
1658
|
}
|
|
1551
1659
|
markdownYText = null;
|
|
1552
1660
|
markdownYjsConnected = false;
|
|
1661
|
+
markdownYjsY = null;
|
|
1553
1662
|
}
|
|
1554
1663
|
|
|
1555
1664
|
function getTextOffsetWithinRoot(root, targetNode, targetOffset) {
|
|
@@ -2637,27 +2746,26 @@ export function generateStudioBridgeScript(options) {
|
|
|
2637
2746
|
const start = editorOffsetToSourceOffset(selection.start, 'start');
|
|
2638
2747
|
const end = editorOffsetToSourceOffset(selection.end, 'end');
|
|
2639
2748
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2749
|
+
// Set local selection on Yjs awareness directly
|
|
2750
|
+
if (markdownYjsConnected && markdownYText && markdownYjsY && markdownYProvider) {
|
|
2751
|
+
var clampedStart = Math.max(0, Math.min(markdownYText.length, start));
|
|
2752
|
+
var clampedEnd = Math.max(0, Math.min(markdownYText.length, end));
|
|
2753
|
+
markdownYProvider.awareness.setLocalStateField('selection', [{
|
|
2754
|
+
anchor: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedStart),
|
|
2755
|
+
marker: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedEnd)
|
|
2756
|
+
}]);
|
|
2757
|
+
markdownPendingSelection = null;
|
|
2758
|
+
} else {
|
|
2759
|
+
// Queue selection for replay after Yjs connects
|
|
2760
|
+
markdownPendingSelection = { start: start, end: end };
|
|
2761
|
+
}
|
|
2647
2762
|
}, 80);
|
|
2648
2763
|
}
|
|
2649
2764
|
|
|
2650
2765
|
function clearMarkdownSelectionSync() {
|
|
2651
|
-
if (
|
|
2652
|
-
|
|
2766
|
+
if (markdownYProvider) {
|
|
2767
|
+
markdownYProvider.awareness.setLocalStateField('selection', null);
|
|
2653
2768
|
}
|
|
2654
|
-
postToStudio({
|
|
2655
|
-
action: 'markdownSelectionChange',
|
|
2656
|
-
fileId: markdownFileId,
|
|
2657
|
-
filePath: PAGE_PATH,
|
|
2658
|
-
start: -1,
|
|
2659
|
-
end: -1
|
|
2660
|
-
});
|
|
2661
2769
|
}
|
|
2662
2770
|
|
|
2663
2771
|
function clearMarkdownSelectionOverlay() {
|
|
@@ -3053,9 +3161,8 @@ export function generateStudioBridgeScript(options) {
|
|
|
3053
3161
|
markdownHasUnsavedChanges = true;
|
|
3054
3162
|
if (markdownYjsConnected) {
|
|
3055
3163
|
syncLocalChangeToYText(fullContent);
|
|
3056
|
-
} else {
|
|
3057
|
-
scheduleMarkdownSync(fullContent);
|
|
3058
3164
|
}
|
|
3165
|
+
scheduleMarkdownSync(fullContent);
|
|
3059
3166
|
scheduleMarkdownSelectionOverlayRender();
|
|
3060
3167
|
}
|
|
3061
3168
|
|
|
@@ -3216,7 +3323,7 @@ export function generateStudioBridgeScript(options) {
|
|
|
3216
3323
|
return;
|
|
3217
3324
|
}
|
|
3218
3325
|
|
|
3219
|
-
if (markdownLexicalApi &&
|
|
3326
|
+
if (markdownLexicalApi && markdownLexicalRenderedContent === content) {
|
|
3220
3327
|
console.debug('[StudioBridge] applyMarkdownContent: skipped (content unchanged)');
|
|
3221
3328
|
markdownCurrentContent = content;
|
|
3222
3329
|
scheduleMarkdownSelectionOverlayRender();
|
|
@@ -3846,6 +3953,15 @@ export function generateStudioBridgeScript(options) {
|
|
|
3846
3953
|
scheduleMarkdownSlashMenuUpdate();
|
|
3847
3954
|
scheduleMarkdownInlineToolbarUpdate();
|
|
3848
3955
|
postMarkdownEditorReady();
|
|
3956
|
+
|
|
3957
|
+
// Self-connect to Yjs when server-injected config is available
|
|
3958
|
+
if (WS_URL && YJS_GUID && !markdownYDoc) {
|
|
3959
|
+
setupMarkdownYjsConnection({
|
|
3960
|
+
wsUrl: WS_URL,
|
|
3961
|
+
guid: YJS_GUID,
|
|
3962
|
+
fileId: markdownFileId
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3849
3965
|
} else {
|
|
3850
3966
|
markdownBody.style.display = '';
|
|
3851
3967
|
if (markdownEditorRoot) {
|
|
@@ -4081,37 +4197,6 @@ export function generateStudioBridgeScript(options) {
|
|
|
4081
4197
|
if (!inspectMode) showHoverOverlay(message.id);
|
|
4082
4198
|
return;
|
|
4083
4199
|
|
|
4084
|
-
case 'setMarkdownContent':
|
|
4085
|
-
if (!isMarkdownPage()) {
|
|
4086
|
-
return;
|
|
4087
|
-
}
|
|
4088
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
4089
|
-
return;
|
|
4090
|
-
}
|
|
4091
|
-
if (markdownYjsConnected) {
|
|
4092
|
-
return;
|
|
4093
|
-
}
|
|
4094
|
-
applyMarkdownContent(message.content || '');
|
|
4095
|
-
return;
|
|
4096
|
-
|
|
4097
|
-
case 'initYjsConnection':
|
|
4098
|
-
if (!isMarkdownPage()) {
|
|
4099
|
-
return;
|
|
4100
|
-
}
|
|
4101
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
4102
|
-
return;
|
|
4103
|
-
}
|
|
4104
|
-
if (message.initialContent) {
|
|
4105
|
-
applyMarkdownContent(message.initialContent);
|
|
4106
|
-
}
|
|
4107
|
-
setupMarkdownYjsConnection({
|
|
4108
|
-
wsUrl: message.wsUrl,
|
|
4109
|
-
guid: message.guid,
|
|
4110
|
-
fileId: message.fileId || markdownFileId,
|
|
4111
|
-
authToken: message.authToken
|
|
4112
|
-
});
|
|
4113
|
-
return;
|
|
4114
|
-
|
|
4115
4200
|
case 'setMarkdownPersistState':
|
|
4116
4201
|
if (!isMarkdownPage()) {
|
|
4117
4202
|
return;
|
|
@@ -4128,26 +4213,6 @@ export function generateStudioBridgeScript(options) {
|
|
|
4128
4213
|
}
|
|
4129
4214
|
return;
|
|
4130
4215
|
|
|
4131
|
-
case 'setMarkdownPresence':
|
|
4132
|
-
if (!isMarkdownPage()) {
|
|
4133
|
-
return;
|
|
4134
|
-
}
|
|
4135
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
4136
|
-
return;
|
|
4137
|
-
}
|
|
4138
|
-
setMarkdownPresence(message.users);
|
|
4139
|
-
return;
|
|
4140
|
-
|
|
4141
|
-
case 'setMarkdownSelections':
|
|
4142
|
-
if (!isMarkdownPage()) {
|
|
4143
|
-
return;
|
|
4144
|
-
}
|
|
4145
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
4146
|
-
return;
|
|
4147
|
-
}
|
|
4148
|
-
setMarkdownSelections(message.selections);
|
|
4149
|
-
return;
|
|
4150
|
-
|
|
4151
4216
|
case 'screenshot':
|
|
4152
4217
|
(async function() {
|
|
4153
4218
|
if (message.multipleSections) {
|
|
@@ -4205,47 +4270,59 @@ export function generateStudioBridgeScript(options) {
|
|
|
4205
4270
|
function init() {
|
|
4206
4271
|
const params = new URLSearchParams(window.location.search);
|
|
4207
4272
|
const studioEmbed = params.get('studio_embed') === 'true';
|
|
4273
|
+
const isStandalone = window.parent === window && !studioEmbed;
|
|
4208
4274
|
|
|
4209
|
-
if (
|
|
4210
|
-
|
|
4211
|
-
|
|
4275
|
+
if (isStandalone) {
|
|
4276
|
+
// Allow standalone markdown editing when WS_URL is available (server-injected Yjs config)
|
|
4277
|
+
if (!WS_URL) {
|
|
4278
|
+
console.debug('[StudioBridge] Not in iframe and not studio_embed mode, skipping initialization');
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4212
4281
|
}
|
|
4213
4282
|
|
|
4214
4283
|
console.debug('[StudioBridge] Initializing...');
|
|
4215
4284
|
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4285
|
+
// Only set up Studio interaction features when embedded in Studio
|
|
4286
|
+
if (!isStandalone) {
|
|
4287
|
+
injectOverlayStyles();
|
|
4288
|
+
hoverOverlay = createOverlay('hover');
|
|
4289
|
+
selectionOverlay = createOverlay('selection');
|
|
4290
|
+
|
|
4291
|
+
setupConsoleCapture();
|
|
4292
|
+
setupErrorHandling();
|
|
4293
|
+
setupInspectMode();
|
|
4294
|
+
}
|
|
4219
4295
|
|
|
4220
|
-
setupConsoleCapture();
|
|
4221
|
-
setupErrorHandling();
|
|
4222
|
-
setupInspectMode();
|
|
4223
4296
|
setupMarkdownEditor(params);
|
|
4224
4297
|
|
|
4225
4298
|
window.addEventListener('message', handleStudioMessage);
|
|
4226
4299
|
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
document.
|
|
4300
|
+
if (!isStandalone) {
|
|
4301
|
+
// IMPORTANT: notifyAppLoaded() must be called BEFORE setupMutationObserver()
|
|
4302
|
+
// because notifyAppLoaded sends onPageTransitionEnd which sets previewId,
|
|
4303
|
+
// and treeUpdated (from setupMutationObserver) requires previewId to be set
|
|
4304
|
+
if (document.readyState === 'loading') {
|
|
4305
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
4306
|
+
notifyAppLoaded();
|
|
4307
|
+
setupMutationObserver();
|
|
4308
|
+
});
|
|
4309
|
+
} else {
|
|
4232
4310
|
notifyAppLoaded();
|
|
4233
4311
|
setupMutationObserver();
|
|
4234
|
-
}
|
|
4235
|
-
} else {
|
|
4236
|
-
notifyAppLoaded();
|
|
4237
|
-
setupMutationObserver();
|
|
4238
|
-
}
|
|
4312
|
+
}
|
|
4239
4313
|
|
|
4240
|
-
|
|
4314
|
+
window.addEventListener('beforeunload', notifyAppUnloaded);
|
|
4315
|
+
}
|
|
4241
4316
|
|
|
4242
4317
|
const colorMode = params.get('color_mode');
|
|
4243
4318
|
if (colorMode) setColorMode(colorMode);
|
|
4244
4319
|
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4320
|
+
if (!isStandalone) {
|
|
4321
|
+
const inspectModeParam = params.get('inspect_mode');
|
|
4322
|
+
if (inspectModeParam === 'true') {
|
|
4323
|
+
inspectMode = true;
|
|
4324
|
+
console.debug('[StudioBridge] Inspect mode enabled from query param');
|
|
4325
|
+
}
|
|
4249
4326
|
}
|
|
4250
4327
|
|
|
4251
4328
|
console.debug('[StudioBridge] Initialized successfully');
|
package/package.json
CHANGED
package/src/deno.js
CHANGED
|
@@ -59,6 +59,10 @@ export interface StudioScriptOptions {
|
|
|
59
59
|
nonce?: string;
|
|
60
60
|
/** Hash of source code for sync detection with Navigator tree */
|
|
61
61
|
sourceHash?: string;
|
|
62
|
+
/** WebSocket URL for direct Yjs connection from the bridge */
|
|
63
|
+
wsUrl?: string;
|
|
64
|
+
/** Yjs document GUID for the bridge to join the same room */
|
|
65
|
+
yjsGuid?: string;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
export function getStudioScripts(options: StudioScriptOptions): string {
|
|
@@ -68,6 +72,8 @@ export function getStudioScripts(options: StudioScriptOptions): string {
|
|
|
68
72
|
projectId: options.projectId,
|
|
69
73
|
pageId: options.pageId,
|
|
70
74
|
...(options.pagePath ? { pagePath: options.pagePath } : {}),
|
|
75
|
+
...(options.wsUrl ? { wsUrl: options.wsUrl } : {}),
|
|
76
|
+
...(options.yjsGuid ? { yjsGuid: options.yjsGuid } : {}),
|
|
71
77
|
}).toString();
|
|
72
78
|
|
|
73
79
|
const sourceHashScript = options.sourceHash
|
|
@@ -23,6 +23,10 @@ export interface InjectHTMLContentOptions {
|
|
|
23
23
|
pageId?: string;
|
|
24
24
|
/** CSP nonce */
|
|
25
25
|
nonce?: string;
|
|
26
|
+
/** WebSocket URL for direct Yjs connection from the bridge */
|
|
27
|
+
wsUrl?: string;
|
|
28
|
+
/** Yjs document GUID for the bridge to join the same room */
|
|
29
|
+
yjsGuid?: string;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export function injectHTMLContent(
|
|
@@ -99,6 +103,8 @@ export function injectHTMLContent(
|
|
|
99
103
|
projectId: options.projectId ?? options.slug,
|
|
100
104
|
pageId: options.pageId ?? options.slug,
|
|
101
105
|
nonce: options.nonce,
|
|
106
|
+
wsUrl: options.wsUrl,
|
|
107
|
+
yjsGuid: options.yjsGuid,
|
|
102
108
|
});
|
|
103
109
|
html = html.replace(/<\/body>/i, `${studioScripts}</body>`);
|
|
104
110
|
}
|
|
@@ -29,6 +29,10 @@ export interface MarkdownHtmlOptions {
|
|
|
29
29
|
projectId: string;
|
|
30
30
|
/** File path of the markdown file. */
|
|
31
31
|
filePath: string;
|
|
32
|
+
/** Branch ID for Yjs room GUID computation. */
|
|
33
|
+
branchId?: string | null;
|
|
34
|
+
/** Request host for computing the WebSocket URL. */
|
|
35
|
+
requestHost?: string;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -58,21 +62,37 @@ function detectTheme(req: dntShim.Request, url: URL): "light" | "dark" | null {
|
|
|
58
62
|
* Injected when embedded in Studio (`studio_embed=true`) or for standalone
|
|
59
63
|
* markdown/MDX pages so the edit button and editor features are available.
|
|
60
64
|
*/
|
|
61
|
-
function buildStudioScript(
|
|
65
|
+
function buildStudioScript(
|
|
66
|
+
url: URL,
|
|
67
|
+
projectId: string,
|
|
68
|
+
filePath: string,
|
|
69
|
+
branchId?: string | null,
|
|
70
|
+
requestHost?: string,
|
|
71
|
+
): string {
|
|
62
72
|
const studioEmbed = url.searchParams.get("studio_embed") === "true";
|
|
63
73
|
const isMarkdown = /\.mdx?$/i.test(filePath);
|
|
64
74
|
if (!studioEmbed && !isMarkdown) return "";
|
|
65
75
|
|
|
66
|
-
const
|
|
76
|
+
const rawQueryProjectId = url.searchParams.get("vf_project_id")?.trim() || "";
|
|
77
|
+
// Validate query param to prevent path traversal in WebSocket URL
|
|
78
|
+
const queryProjectId = /^[a-zA-Z0-9_-]+$/.test(rawQueryProjectId) ? rawQueryProjectId : "";
|
|
67
79
|
const queryFileId = url.searchParams.get("vf_file_id")?.trim() || "";
|
|
68
80
|
const canonicalProjectId = queryProjectId || projectId;
|
|
69
81
|
const canonicalPageId = queryFileId || filePath;
|
|
70
82
|
|
|
83
|
+
// Compute Yjs connection config for the bridge to self-connect
|
|
84
|
+
const wsProtocol = url.protocol === "https:" ? "wss" : "ws";
|
|
85
|
+
const host = requestHost || url.host;
|
|
86
|
+
const wsUrl = `${wsProtocol}://${host}/api/ws/${canonicalProjectId}/yjs`;
|
|
87
|
+
const yjsGuid = branchId ? `${canonicalProjectId}:${branchId}` : canonicalProjectId;
|
|
88
|
+
|
|
71
89
|
return `<script>${
|
|
72
90
|
generateStudioBridgeScript({
|
|
73
91
|
projectId: canonicalProjectId,
|
|
74
92
|
pageId: canonicalPageId,
|
|
75
93
|
pagePath: filePath,
|
|
94
|
+
wsUrl,
|
|
95
|
+
yjsGuid,
|
|
76
96
|
})
|
|
77
97
|
}</script>`;
|
|
78
98
|
}
|
|
@@ -85,10 +105,11 @@ function buildStudioScript(url: URL, projectId: string, filePath: string): strin
|
|
|
85
105
|
* studio bridge integration.
|
|
86
106
|
*/
|
|
87
107
|
export function generateMarkdownHtml(options: MarkdownHtmlOptions): string {
|
|
88
|
-
const { rawHtml, title, description, request, url, projectId, filePath } =
|
|
108
|
+
const { rawHtml, title, description, request, url, projectId, filePath, branchId, requestHost } =
|
|
109
|
+
options;
|
|
89
110
|
|
|
90
111
|
const theme = detectTheme(request, url);
|
|
91
|
-
const studioScript = buildStudioScript(url, projectId, filePath);
|
|
112
|
+
const studioScript = buildStudioScript(url, projectId, filePath, branchId, requestHost);
|
|
92
113
|
const themeAttrs = theme ? ` data-theme="${theme}" style="color-scheme: ${theme};"` : "";
|
|
93
114
|
|
|
94
115
|
return `<!DOCTYPE html>
|
|
@@ -159,6 +159,8 @@ export class MarkdownPreviewHandler extends BaseHandler {
|
|
|
159
159
|
url,
|
|
160
160
|
projectId: ctx.projectSlug || ctx.projectId || "markdown-preview",
|
|
161
161
|
filePath,
|
|
162
|
+
branchId: ctx.parsedDomain?.branch ?? null,
|
|
163
|
+
requestHost: url.host,
|
|
162
164
|
});
|
|
163
165
|
|
|
164
166
|
const responseBuilder = this.createResponseBuilder(ctx)
|
|
@@ -38,8 +38,10 @@ export class StudioEndpointsHandler extends BaseHandler {
|
|
|
38
38
|
const projectId = url.searchParams.get("projectId") ?? "";
|
|
39
39
|
const pageId = url.searchParams.get("pageId") ?? "";
|
|
40
40
|
const pagePath = url.searchParams.get("pagePath") ?? undefined;
|
|
41
|
+
const wsUrl = url.searchParams.get("wsUrl") ?? undefined;
|
|
42
|
+
const yjsGuid = url.searchParams.get("yjsGuid") ?? undefined;
|
|
41
43
|
|
|
42
|
-
const script = generateStudioBridgeScript({ projectId, pageId, pagePath });
|
|
44
|
+
const script = generateStudioBridgeScript({ projectId, pageId, pagePath, wsUrl, yjsGuid });
|
|
43
45
|
const response = builder.withCache("no-cache").javascript(script, HTTP_OK);
|
|
44
46
|
|
|
45
47
|
return Promise.resolve(this.respond(response));
|
|
@@ -2,6 +2,8 @@ export interface StudioBridgeOptions {
|
|
|
2
2
|
projectId: string;
|
|
3
3
|
pageId: string;
|
|
4
4
|
pagePath?: string;
|
|
5
|
+
wsUrl?: string;
|
|
6
|
+
yjsGuid?: string;
|
|
5
7
|
debugSkipInit?: boolean;
|
|
6
8
|
debugExposeInternals?: boolean;
|
|
7
9
|
}
|
|
@@ -13,6 +15,8 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
13
15
|
const PROJECT_ID = ${JSON.stringify(options.projectId)};
|
|
14
16
|
const PAGE_ID = ${JSON.stringify(options.pageId)};
|
|
15
17
|
const PAGE_PATH = ${JSON.stringify(options.pagePath ?? options.pageId)};
|
|
18
|
+
const WS_URL = ${JSON.stringify(options.wsUrl ?? "")};
|
|
19
|
+
const YJS_GUID = ${JSON.stringify(options.yjsGuid ?? "")};
|
|
16
20
|
const DEBUG_SKIP_INIT = ${options.debugSkipInit ? "true" : "false"};
|
|
17
21
|
const DEBUG_EXPOSE_INTERNALS = ${options.debugExposeInternals ? "true" : "false"};
|
|
18
22
|
|
|
@@ -83,6 +87,8 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
83
87
|
let markdownYText = null;
|
|
84
88
|
let markdownYjsConnected = false;
|
|
85
89
|
let markdownYjsSetupId = 0;
|
|
90
|
+
let markdownYjsY = null;
|
|
91
|
+
let markdownPendingSelection = null;
|
|
86
92
|
const LEXICAL_YJS_ORIGIN = 'lexical-yjs-binding';
|
|
87
93
|
|
|
88
94
|
const MARKDOWN_SLASH_COMMANDS = [
|
|
@@ -1474,7 +1480,7 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
1474
1480
|
|
|
1475
1481
|
Promise.all([
|
|
1476
1482
|
import('https://esm.sh/yjs@13.6.28?target=es2022'),
|
|
1477
|
-
import('https://esm.sh/y-websocket@2.1.0?target=es2022')
|
|
1483
|
+
import('https://esm.sh/y-websocket@2.1.0?deps=yjs@13.6.28&target=es2022')
|
|
1478
1484
|
]).then(function(modules) {
|
|
1479
1485
|
// Abort if edit mode was closed while imports were loading
|
|
1480
1486
|
if (setupId !== markdownYjsSetupId) {
|
|
@@ -1483,11 +1489,13 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
1483
1489
|
|
|
1484
1490
|
var Y = modules[0];
|
|
1485
1491
|
var WebsocketProvider = modules[1].WebsocketProvider;
|
|
1492
|
+
markdownYjsY = Y;
|
|
1486
1493
|
|
|
1487
1494
|
var doc = new Y.Doc({ guid: config.guid });
|
|
1495
|
+
// Cookie auth: authToken cookie on .veryfront.com is sent automatically
|
|
1496
|
+
// with the WebSocket upgrade request. No explicit token param needed.
|
|
1488
1497
|
var provider = new WebsocketProvider(config.wsUrl, config.guid, doc, {
|
|
1489
|
-
resyncInterval: -1
|
|
1490
|
-
params: { token: config.authToken }
|
|
1498
|
+
resyncInterval: -1
|
|
1491
1499
|
});
|
|
1492
1500
|
|
|
1493
1501
|
var ytext = doc.getText(config.fileId);
|
|
@@ -1512,6 +1520,93 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
1512
1520
|
}
|
|
1513
1521
|
});
|
|
1514
1522
|
|
|
1523
|
+
// Extract user identity from authToken JWT cookie for presence
|
|
1524
|
+
var presenceUser = { id: 'preview-' + Math.random().toString(36).slice(2), name: 'Preview' };
|
|
1525
|
+
try {
|
|
1526
|
+
var cookieMatch = document.cookie.match(/authToken=([^;]+)/);
|
|
1527
|
+
if (cookieMatch) {
|
|
1528
|
+
var parts = cookieMatch[1].split('.');
|
|
1529
|
+
if (parts.length === 3) {
|
|
1530
|
+
var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
1531
|
+
if (payload.userId) {
|
|
1532
|
+
presenceUser.id = payload.userId;
|
|
1533
|
+
}
|
|
1534
|
+
if (payload.email) {
|
|
1535
|
+
var local = payload.email.split('@')[0] || '';
|
|
1536
|
+
if (local.includes('.') || local.includes('_')) {
|
|
1537
|
+
presenceUser.name = local.split(/[._]/).map(function(p) { return p.charAt(0).toUpperCase() + p.slice(1); }).join(' ');
|
|
1538
|
+
} else {
|
|
1539
|
+
presenceUser.name = local.charAt(0).toUpperCase() + local.slice(1);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
} catch (e) {
|
|
1545
|
+
// Fall back to defaults on any parse error
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Set local user on awareness for presence
|
|
1549
|
+
provider.awareness.setLocalStateField('user', {
|
|
1550
|
+
id: presenceUser.id,
|
|
1551
|
+
name: presenceUser.name,
|
|
1552
|
+
color: '#10b981'
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
// Observe awareness for remote presence and selection changes
|
|
1556
|
+
function syncAwareness() {
|
|
1557
|
+
var states = Array.from(provider.awareness.getStates().entries());
|
|
1558
|
+
|
|
1559
|
+
// Sync presence users
|
|
1560
|
+
var users = [];
|
|
1561
|
+
for (var i = 0; i < states.length; i++) {
|
|
1562
|
+
var clientId = states[i][0];
|
|
1563
|
+
var state = states[i][1];
|
|
1564
|
+
var user = state.user;
|
|
1565
|
+
if (!user || typeof user.name !== 'string') {
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
users.push({
|
|
1569
|
+
id: user.id || String(clientId),
|
|
1570
|
+
name: user.name,
|
|
1571
|
+
color: user.color || '#6b7280',
|
|
1572
|
+
isCurrentUser: clientId === provider.awareness.clientID,
|
|
1573
|
+
isAgent: user.isAgent || false
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
setMarkdownPresence(users);
|
|
1577
|
+
|
|
1578
|
+
// Sync remote selections
|
|
1579
|
+
var selections = [];
|
|
1580
|
+
for (var j = 0; j < states.length; j++) {
|
|
1581
|
+
var cId = states[j][0];
|
|
1582
|
+
var st = states[j][1];
|
|
1583
|
+
var u = st.user;
|
|
1584
|
+
var ranges = st.selection;
|
|
1585
|
+
if (!u || !Array.isArray(ranges) || ranges.length === 0) {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
for (var k = 0; k < ranges.length; k++) {
|
|
1589
|
+
var range = ranges[k];
|
|
1590
|
+
var anchorPos = Y.createAbsolutePositionFromRelativePosition(range.anchor, doc);
|
|
1591
|
+
var markerPos = Y.createAbsolutePositionFromRelativePosition(range.marker, doc);
|
|
1592
|
+
if (!anchorPos || !markerPos || anchorPos.type !== ytext || markerPos.type !== ytext) {
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
selections.push({
|
|
1596
|
+
id: u.id || String(cId),
|
|
1597
|
+
name: u.name || 'Anonymous',
|
|
1598
|
+
color: u.color || '#6b7280',
|
|
1599
|
+
isCurrentUser: cId === provider.awareness.clientID,
|
|
1600
|
+
start: Math.min(anchorPos.index, markerPos.index),
|
|
1601
|
+
end: Math.max(anchorPos.index, markerPos.index)
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
setMarkdownSelections(selections);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
provider.awareness.on('change', syncAwareness);
|
|
1609
|
+
|
|
1515
1610
|
provider.on('sync', function(synced) {
|
|
1516
1611
|
if (synced && !markdownYjsConnected) {
|
|
1517
1612
|
markdownYjsConnected = true;
|
|
@@ -1525,6 +1620,18 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
1525
1620
|
applyMarkdownContent(ytextContent);
|
|
1526
1621
|
}
|
|
1527
1622
|
|
|
1623
|
+
// Replay any selection queued before Yjs was ready
|
|
1624
|
+
if (markdownPendingSelection) {
|
|
1625
|
+
var ps = markdownPendingSelection;
|
|
1626
|
+
markdownPendingSelection = null;
|
|
1627
|
+
var cs = Math.max(0, Math.min(ytext.length, ps.start));
|
|
1628
|
+
var ce = Math.max(0, Math.min(ytext.length, ps.end));
|
|
1629
|
+
provider.awareness.setLocalStateField('selection', [{
|
|
1630
|
+
anchor: Y.createRelativePositionFromTypeIndex(ytext, cs),
|
|
1631
|
+
marker: Y.createRelativePositionFromTypeIndex(ytext, ce)
|
|
1632
|
+
}]);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1528
1635
|
// Observe Y.Text for remote changes (from other users / Monaco)
|
|
1529
1636
|
ytext.observe(function(event) {
|
|
1530
1637
|
if (event.transaction.origin === LEXICAL_YJS_ORIGIN) {
|
|
@@ -1537,6 +1644,9 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
1537
1644
|
applyMarkdownContent(fullContent);
|
|
1538
1645
|
});
|
|
1539
1646
|
|
|
1647
|
+
// Initial awareness sync after Yjs is connected
|
|
1648
|
+
syncAwareness();
|
|
1649
|
+
|
|
1540
1650
|
console.debug('[StudioBridge] Yjs synced, bound to Y.Text for fileId:', config.fileId);
|
|
1541
1651
|
}
|
|
1542
1652
|
});
|
|
@@ -1558,6 +1668,7 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
1558
1668
|
}
|
|
1559
1669
|
markdownYText = null;
|
|
1560
1670
|
markdownYjsConnected = false;
|
|
1671
|
+
markdownYjsY = null;
|
|
1561
1672
|
}
|
|
1562
1673
|
|
|
1563
1674
|
function getTextOffsetWithinRoot(root, targetNode, targetOffset) {
|
|
@@ -2645,27 +2756,26 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
2645
2756
|
const start = editorOffsetToSourceOffset(selection.start, 'start');
|
|
2646
2757
|
const end = editorOffsetToSourceOffset(selection.end, 'end');
|
|
2647
2758
|
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2759
|
+
// Set local selection on Yjs awareness directly
|
|
2760
|
+
if (markdownYjsConnected && markdownYText && markdownYjsY && markdownYProvider) {
|
|
2761
|
+
var clampedStart = Math.max(0, Math.min(markdownYText.length, start));
|
|
2762
|
+
var clampedEnd = Math.max(0, Math.min(markdownYText.length, end));
|
|
2763
|
+
markdownYProvider.awareness.setLocalStateField('selection', [{
|
|
2764
|
+
anchor: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedStart),
|
|
2765
|
+
marker: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedEnd)
|
|
2766
|
+
}]);
|
|
2767
|
+
markdownPendingSelection = null;
|
|
2768
|
+
} else {
|
|
2769
|
+
// Queue selection for replay after Yjs connects
|
|
2770
|
+
markdownPendingSelection = { start: start, end: end };
|
|
2771
|
+
}
|
|
2655
2772
|
}, 80);
|
|
2656
2773
|
}
|
|
2657
2774
|
|
|
2658
2775
|
function clearMarkdownSelectionSync() {
|
|
2659
|
-
if (
|
|
2660
|
-
|
|
2776
|
+
if (markdownYProvider) {
|
|
2777
|
+
markdownYProvider.awareness.setLocalStateField('selection', null);
|
|
2661
2778
|
}
|
|
2662
|
-
postToStudio({
|
|
2663
|
-
action: 'markdownSelectionChange',
|
|
2664
|
-
fileId: markdownFileId,
|
|
2665
|
-
filePath: PAGE_PATH,
|
|
2666
|
-
start: -1,
|
|
2667
|
-
end: -1
|
|
2668
|
-
});
|
|
2669
2779
|
}
|
|
2670
2780
|
|
|
2671
2781
|
function clearMarkdownSelectionOverlay() {
|
|
@@ -3061,9 +3171,8 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
3061
3171
|
markdownHasUnsavedChanges = true;
|
|
3062
3172
|
if (markdownYjsConnected) {
|
|
3063
3173
|
syncLocalChangeToYText(fullContent);
|
|
3064
|
-
} else {
|
|
3065
|
-
scheduleMarkdownSync(fullContent);
|
|
3066
3174
|
}
|
|
3175
|
+
scheduleMarkdownSync(fullContent);
|
|
3067
3176
|
scheduleMarkdownSelectionOverlayRender();
|
|
3068
3177
|
}
|
|
3069
3178
|
|
|
@@ -3224,7 +3333,7 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
3224
3333
|
return;
|
|
3225
3334
|
}
|
|
3226
3335
|
|
|
3227
|
-
if (markdownLexicalApi &&
|
|
3336
|
+
if (markdownLexicalApi && markdownLexicalRenderedContent === content) {
|
|
3228
3337
|
console.debug('[StudioBridge] applyMarkdownContent: skipped (content unchanged)');
|
|
3229
3338
|
markdownCurrentContent = content;
|
|
3230
3339
|
scheduleMarkdownSelectionOverlayRender();
|
|
@@ -3854,6 +3963,15 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
3854
3963
|
scheduleMarkdownSlashMenuUpdate();
|
|
3855
3964
|
scheduleMarkdownInlineToolbarUpdate();
|
|
3856
3965
|
postMarkdownEditorReady();
|
|
3966
|
+
|
|
3967
|
+
// Self-connect to Yjs when server-injected config is available
|
|
3968
|
+
if (WS_URL && YJS_GUID && !markdownYDoc) {
|
|
3969
|
+
setupMarkdownYjsConnection({
|
|
3970
|
+
wsUrl: WS_URL,
|
|
3971
|
+
guid: YJS_GUID,
|
|
3972
|
+
fileId: markdownFileId
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3857
3975
|
} else {
|
|
3858
3976
|
markdownBody.style.display = '';
|
|
3859
3977
|
if (markdownEditorRoot) {
|
|
@@ -4089,37 +4207,6 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
4089
4207
|
if (!inspectMode) showHoverOverlay(message.id);
|
|
4090
4208
|
return;
|
|
4091
4209
|
|
|
4092
|
-
case 'setMarkdownContent':
|
|
4093
|
-
if (!isMarkdownPage()) {
|
|
4094
|
-
return;
|
|
4095
|
-
}
|
|
4096
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
4097
|
-
return;
|
|
4098
|
-
}
|
|
4099
|
-
if (markdownYjsConnected) {
|
|
4100
|
-
return;
|
|
4101
|
-
}
|
|
4102
|
-
applyMarkdownContent(message.content || '');
|
|
4103
|
-
return;
|
|
4104
|
-
|
|
4105
|
-
case 'initYjsConnection':
|
|
4106
|
-
if (!isMarkdownPage()) {
|
|
4107
|
-
return;
|
|
4108
|
-
}
|
|
4109
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
4110
|
-
return;
|
|
4111
|
-
}
|
|
4112
|
-
if (message.initialContent) {
|
|
4113
|
-
applyMarkdownContent(message.initialContent);
|
|
4114
|
-
}
|
|
4115
|
-
setupMarkdownYjsConnection({
|
|
4116
|
-
wsUrl: message.wsUrl,
|
|
4117
|
-
guid: message.guid,
|
|
4118
|
-
fileId: message.fileId || markdownFileId,
|
|
4119
|
-
authToken: message.authToken
|
|
4120
|
-
});
|
|
4121
|
-
return;
|
|
4122
|
-
|
|
4123
4210
|
case 'setMarkdownPersistState':
|
|
4124
4211
|
if (!isMarkdownPage()) {
|
|
4125
4212
|
return;
|
|
@@ -4136,26 +4223,6 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
4136
4223
|
}
|
|
4137
4224
|
return;
|
|
4138
4225
|
|
|
4139
|
-
case 'setMarkdownPresence':
|
|
4140
|
-
if (!isMarkdownPage()) {
|
|
4141
|
-
return;
|
|
4142
|
-
}
|
|
4143
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
4144
|
-
return;
|
|
4145
|
-
}
|
|
4146
|
-
setMarkdownPresence(message.users);
|
|
4147
|
-
return;
|
|
4148
|
-
|
|
4149
|
-
case 'setMarkdownSelections':
|
|
4150
|
-
if (!isMarkdownPage()) {
|
|
4151
|
-
return;
|
|
4152
|
-
}
|
|
4153
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
4154
|
-
return;
|
|
4155
|
-
}
|
|
4156
|
-
setMarkdownSelections(message.selections);
|
|
4157
|
-
return;
|
|
4158
|
-
|
|
4159
4226
|
case 'screenshot':
|
|
4160
4227
|
(async function() {
|
|
4161
4228
|
if (message.multipleSections) {
|
|
@@ -4213,47 +4280,59 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
4213
4280
|
function init() {
|
|
4214
4281
|
const params = new URLSearchParams(window.location.search);
|
|
4215
4282
|
const studioEmbed = params.get('studio_embed') === 'true';
|
|
4283
|
+
const isStandalone = window.parent === window && !studioEmbed;
|
|
4216
4284
|
|
|
4217
|
-
if (
|
|
4218
|
-
|
|
4219
|
-
|
|
4285
|
+
if (isStandalone) {
|
|
4286
|
+
// Allow standalone markdown editing when WS_URL is available (server-injected Yjs config)
|
|
4287
|
+
if (!WS_URL) {
|
|
4288
|
+
console.debug('[StudioBridge] Not in iframe and not studio_embed mode, skipping initialization');
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4220
4291
|
}
|
|
4221
4292
|
|
|
4222
4293
|
console.debug('[StudioBridge] Initializing...');
|
|
4223
4294
|
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4295
|
+
// Only set up Studio interaction features when embedded in Studio
|
|
4296
|
+
if (!isStandalone) {
|
|
4297
|
+
injectOverlayStyles();
|
|
4298
|
+
hoverOverlay = createOverlay('hover');
|
|
4299
|
+
selectionOverlay = createOverlay('selection');
|
|
4300
|
+
|
|
4301
|
+
setupConsoleCapture();
|
|
4302
|
+
setupErrorHandling();
|
|
4303
|
+
setupInspectMode();
|
|
4304
|
+
}
|
|
4227
4305
|
|
|
4228
|
-
setupConsoleCapture();
|
|
4229
|
-
setupErrorHandling();
|
|
4230
|
-
setupInspectMode();
|
|
4231
4306
|
setupMarkdownEditor(params);
|
|
4232
4307
|
|
|
4233
4308
|
window.addEventListener('message', handleStudioMessage);
|
|
4234
4309
|
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
document.
|
|
4310
|
+
if (!isStandalone) {
|
|
4311
|
+
// IMPORTANT: notifyAppLoaded() must be called BEFORE setupMutationObserver()
|
|
4312
|
+
// because notifyAppLoaded sends onPageTransitionEnd which sets previewId,
|
|
4313
|
+
// and treeUpdated (from setupMutationObserver) requires previewId to be set
|
|
4314
|
+
if (document.readyState === 'loading') {
|
|
4315
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
4316
|
+
notifyAppLoaded();
|
|
4317
|
+
setupMutationObserver();
|
|
4318
|
+
});
|
|
4319
|
+
} else {
|
|
4240
4320
|
notifyAppLoaded();
|
|
4241
4321
|
setupMutationObserver();
|
|
4242
|
-
}
|
|
4243
|
-
} else {
|
|
4244
|
-
notifyAppLoaded();
|
|
4245
|
-
setupMutationObserver();
|
|
4246
|
-
}
|
|
4322
|
+
}
|
|
4247
4323
|
|
|
4248
|
-
|
|
4324
|
+
window.addEventListener('beforeunload', notifyAppUnloaded);
|
|
4325
|
+
}
|
|
4249
4326
|
|
|
4250
4327
|
const colorMode = params.get('color_mode');
|
|
4251
4328
|
if (colorMode) setColorMode(colorMode);
|
|
4252
4329
|
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4330
|
+
if (!isStandalone) {
|
|
4331
|
+
const inspectModeParam = params.get('inspect_mode');
|
|
4332
|
+
if (inspectModeParam === 'true') {
|
|
4333
|
+
inspectMode = true;
|
|
4334
|
+
console.debug('[StudioBridge] Inspect mode enabled from query param');
|
|
4335
|
+
}
|
|
4257
4336
|
}
|
|
4258
4337
|
|
|
4259
4338
|
console.debug('[StudioBridge] Initialized successfully');
|