veryfront 0.1.30 → 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 +316 -71
- 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 +318 -71
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
|
|
|
@@ -70,6 +72,14 @@ export function generateStudioBridgeScript(options) {
|
|
|
70
72
|
let markdownLatestSelections = [];
|
|
71
73
|
let markdownHasUnsavedChanges = false;
|
|
72
74
|
let markdownSaveInProgress = false;
|
|
75
|
+
let markdownYDoc = null;
|
|
76
|
+
let markdownYProvider = null;
|
|
77
|
+
let markdownYText = null;
|
|
78
|
+
let markdownYjsConnected = false;
|
|
79
|
+
let markdownYjsSetupId = 0;
|
|
80
|
+
let markdownYjsY = null;
|
|
81
|
+
let markdownPendingSelection = null;
|
|
82
|
+
const LEXICAL_YJS_ORIGIN = 'lexical-yjs-binding';
|
|
73
83
|
|
|
74
84
|
const MARKDOWN_SLASH_COMMANDS = [
|
|
75
85
|
{
|
|
@@ -1410,6 +1420,247 @@ export function generateStudioBridgeScript(options) {
|
|
|
1410
1420
|
}, 120);
|
|
1411
1421
|
}
|
|
1412
1422
|
|
|
1423
|
+
function computeTextDiff(oldText, newText) {
|
|
1424
|
+
var prefixLen = 0;
|
|
1425
|
+
var minLen = Math.min(oldText.length, newText.length);
|
|
1426
|
+
while (prefixLen < minLen && oldText.charCodeAt(prefixLen) === newText.charCodeAt(prefixLen)) {
|
|
1427
|
+
prefixLen++;
|
|
1428
|
+
}
|
|
1429
|
+
var suffixLen = 0;
|
|
1430
|
+
var maxSuffix = minLen - prefixLen;
|
|
1431
|
+
while (suffixLen < maxSuffix &&
|
|
1432
|
+
oldText.charCodeAt(oldText.length - 1 - suffixLen) === newText.charCodeAt(newText.length - 1 - suffixLen)) {
|
|
1433
|
+
suffixLen++;
|
|
1434
|
+
}
|
|
1435
|
+
return {
|
|
1436
|
+
index: prefixLen,
|
|
1437
|
+
deleteCount: oldText.length - prefixLen - suffixLen,
|
|
1438
|
+
insertText: newText.slice(prefixLen, suffixLen > 0 ? newText.length - suffixLen : undefined)
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function syncLocalChangeToYText(fullContent) {
|
|
1443
|
+
if (!markdownYText || !markdownYDoc) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
var currentYContent = markdownYText.toString();
|
|
1447
|
+
if (currentYContent === fullContent) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
var diff = computeTextDiff(currentYContent, fullContent);
|
|
1451
|
+
if (diff.deleteCount === 0 && diff.insertText === '') {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
markdownYDoc.transact(function() {
|
|
1455
|
+
if (diff.deleteCount > 0) {
|
|
1456
|
+
markdownYText.delete(diff.index, diff.deleteCount);
|
|
1457
|
+
}
|
|
1458
|
+
if (diff.insertText) {
|
|
1459
|
+
markdownYText.insert(diff.index, diff.insertText);
|
|
1460
|
+
}
|
|
1461
|
+
}, LEXICAL_YJS_ORIGIN);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function setupMarkdownYjsConnection(config) {
|
|
1465
|
+
if (markdownYDoc) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
var setupId = ++markdownYjsSetupId;
|
|
1470
|
+
|
|
1471
|
+
Promise.all([
|
|
1472
|
+
import('https://esm.sh/yjs@13.6.28?target=es2022'),
|
|
1473
|
+
import('https://esm.sh/y-websocket@2.1.0?deps=yjs@13.6.28&target=es2022')
|
|
1474
|
+
]).then(function(modules) {
|
|
1475
|
+
// Abort if edit mode was closed while imports were loading
|
|
1476
|
+
if (setupId !== markdownYjsSetupId) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
var Y = modules[0];
|
|
1481
|
+
var WebsocketProvider = modules[1].WebsocketProvider;
|
|
1482
|
+
markdownYjsY = Y;
|
|
1483
|
+
|
|
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.
|
|
1487
|
+
var provider = new WebsocketProvider(config.wsUrl, config.guid, doc, {
|
|
1488
|
+
resyncInterval: -1
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
var ytext = doc.getText(config.fileId);
|
|
1492
|
+
|
|
1493
|
+
markdownYDoc = doc;
|
|
1494
|
+
markdownYProvider = provider;
|
|
1495
|
+
markdownYText = ytext;
|
|
1496
|
+
|
|
1497
|
+
// Filter non-binary messages to prevent y-websocket parse errors
|
|
1498
|
+
provider.on('status', function(event) {
|
|
1499
|
+
console.debug('[StudioBridge] Yjs status:', event.status);
|
|
1500
|
+
if (event.status === 'connected' && provider.ws) {
|
|
1501
|
+
var origOnMessage = provider.ws.onmessage;
|
|
1502
|
+
provider.ws.onmessage = function(wsEvent) {
|
|
1503
|
+
if (typeof wsEvent.data === 'string') {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if (origOnMessage) {
|
|
1507
|
+
origOnMessage.call(provider.ws, wsEvent);
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
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
|
+
|
|
1600
|
+
provider.on('sync', function(synced) {
|
|
1601
|
+
if (synced && !markdownYjsConnected) {
|
|
1602
|
+
markdownYjsConnected = true;
|
|
1603
|
+
|
|
1604
|
+
var ytextContent = ytext.toString();
|
|
1605
|
+
if (markdownCurrentContent && markdownCurrentContent !== ytextContent) {
|
|
1606
|
+
// User typed before sync completed — push local edits to Y.Text
|
|
1607
|
+
syncLocalChangeToYText(markdownCurrentContent);
|
|
1608
|
+
} else if (ytextContent) {
|
|
1609
|
+
// No local edits — seed editor from Y.Text
|
|
1610
|
+
applyMarkdownContent(ytextContent);
|
|
1611
|
+
}
|
|
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
|
+
|
|
1625
|
+
// Observe Y.Text for remote changes (from other users / Monaco)
|
|
1626
|
+
ytext.observe(function(event) {
|
|
1627
|
+
if (event.transaction.origin === LEXICAL_YJS_ORIGIN) {
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
var fullContent = ytext.toString();
|
|
1631
|
+
if (fullContent === markdownCurrentContent) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
applyMarkdownContent(fullContent);
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
// Initial awareness sync after Yjs is connected
|
|
1638
|
+
syncAwareness();
|
|
1639
|
+
|
|
1640
|
+
console.debug('[StudioBridge] Yjs synced, bound to Y.Text for fileId:', config.fileId);
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
}).catch(function(error) {
|
|
1644
|
+
console.error('[StudioBridge] Failed to setup Yjs connection:', error);
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function disposeMarkdownYjs() {
|
|
1649
|
+
markdownYjsSetupId++;
|
|
1650
|
+
if (markdownYProvider) {
|
|
1651
|
+
markdownYProvider.disconnect();
|
|
1652
|
+
markdownYProvider.destroy();
|
|
1653
|
+
markdownYProvider = null;
|
|
1654
|
+
}
|
|
1655
|
+
if (markdownYDoc) {
|
|
1656
|
+
markdownYDoc.destroy();
|
|
1657
|
+
markdownYDoc = null;
|
|
1658
|
+
}
|
|
1659
|
+
markdownYText = null;
|
|
1660
|
+
markdownYjsConnected = false;
|
|
1661
|
+
markdownYjsY = null;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1413
1664
|
function getTextOffsetWithinRoot(root, targetNode, targetOffset) {
|
|
1414
1665
|
if (!root || !targetNode) {
|
|
1415
1666
|
return 0;
|
|
@@ -2495,27 +2746,26 @@ export function generateStudioBridgeScript(options) {
|
|
|
2495
2746
|
const start = editorOffsetToSourceOffset(selection.start, 'start');
|
|
2496
2747
|
const end = editorOffsetToSourceOffset(selection.end, 'end');
|
|
2497
2748
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
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
|
+
}
|
|
2505
2762
|
}, 80);
|
|
2506
2763
|
}
|
|
2507
2764
|
|
|
2508
2765
|
function clearMarkdownSelectionSync() {
|
|
2509
|
-
if (
|
|
2510
|
-
|
|
2766
|
+
if (markdownYProvider) {
|
|
2767
|
+
markdownYProvider.awareness.setLocalStateField('selection', null);
|
|
2511
2768
|
}
|
|
2512
|
-
postToStudio({
|
|
2513
|
-
action: 'markdownSelectionChange',
|
|
2514
|
-
fileId: markdownFileId,
|
|
2515
|
-
filePath: PAGE_PATH,
|
|
2516
|
-
start: -1,
|
|
2517
|
-
end: -1
|
|
2518
|
-
});
|
|
2519
2769
|
}
|
|
2520
2770
|
|
|
2521
2771
|
function clearMarkdownSelectionOverlay() {
|
|
@@ -2909,6 +3159,9 @@ export function generateStudioBridgeScript(options) {
|
|
|
2909
3159
|
}
|
|
2910
3160
|
markdownCurrentContent = fullContent;
|
|
2911
3161
|
markdownHasUnsavedChanges = true;
|
|
3162
|
+
if (markdownYjsConnected) {
|
|
3163
|
+
syncLocalChangeToYText(fullContent);
|
|
3164
|
+
}
|
|
2912
3165
|
scheduleMarkdownSync(fullContent);
|
|
2913
3166
|
scheduleMarkdownSelectionOverlayRender();
|
|
2914
3167
|
}
|
|
@@ -3070,7 +3323,7 @@ export function generateStudioBridgeScript(options) {
|
|
|
3070
3323
|
return;
|
|
3071
3324
|
}
|
|
3072
3325
|
|
|
3073
|
-
if (markdownLexicalApi &&
|
|
3326
|
+
if (markdownLexicalApi && markdownLexicalRenderedContent === content) {
|
|
3074
3327
|
console.debug('[StudioBridge] applyMarkdownContent: skipped (content unchanged)');
|
|
3075
3328
|
markdownCurrentContent = content;
|
|
3076
3329
|
scheduleMarkdownSelectionOverlayRender();
|
|
@@ -3700,6 +3953,15 @@ export function generateStudioBridgeScript(options) {
|
|
|
3700
3953
|
scheduleMarkdownSlashMenuUpdate();
|
|
3701
3954
|
scheduleMarkdownInlineToolbarUpdate();
|
|
3702
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
|
+
}
|
|
3703
3965
|
} else {
|
|
3704
3966
|
markdownBody.style.display = '';
|
|
3705
3967
|
if (markdownEditorRoot) {
|
|
@@ -3711,6 +3973,7 @@ export function generateStudioBridgeScript(options) {
|
|
|
3711
3973
|
markdownOverlaySelections = [];
|
|
3712
3974
|
clearMarkdownSelectionOverlay();
|
|
3713
3975
|
clearMarkdownSelectionSync();
|
|
3976
|
+
disposeMarkdownYjs();
|
|
3714
3977
|
}
|
|
3715
3978
|
|
|
3716
3979
|
const nextUrl = new URL(window.location.href);
|
|
@@ -3934,16 +4197,6 @@ export function generateStudioBridgeScript(options) {
|
|
|
3934
4197
|
if (!inspectMode) showHoverOverlay(message.id);
|
|
3935
4198
|
return;
|
|
3936
4199
|
|
|
3937
|
-
case 'setMarkdownContent':
|
|
3938
|
-
if (!isMarkdownPage()) {
|
|
3939
|
-
return;
|
|
3940
|
-
}
|
|
3941
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
3942
|
-
return;
|
|
3943
|
-
}
|
|
3944
|
-
applyMarkdownContent(message.content || '');
|
|
3945
|
-
return;
|
|
3946
|
-
|
|
3947
4200
|
case 'setMarkdownPersistState':
|
|
3948
4201
|
if (!isMarkdownPage()) {
|
|
3949
4202
|
return;
|
|
@@ -3960,26 +4213,6 @@ export function generateStudioBridgeScript(options) {
|
|
|
3960
4213
|
}
|
|
3961
4214
|
return;
|
|
3962
4215
|
|
|
3963
|
-
case 'setMarkdownPresence':
|
|
3964
|
-
if (!isMarkdownPage()) {
|
|
3965
|
-
return;
|
|
3966
|
-
}
|
|
3967
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
3968
|
-
return;
|
|
3969
|
-
}
|
|
3970
|
-
setMarkdownPresence(message.users);
|
|
3971
|
-
return;
|
|
3972
|
-
|
|
3973
|
-
case 'setMarkdownSelections':
|
|
3974
|
-
if (!isMarkdownPage()) {
|
|
3975
|
-
return;
|
|
3976
|
-
}
|
|
3977
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
3978
|
-
return;
|
|
3979
|
-
}
|
|
3980
|
-
setMarkdownSelections(message.selections);
|
|
3981
|
-
return;
|
|
3982
|
-
|
|
3983
4216
|
case 'screenshot':
|
|
3984
4217
|
(async function() {
|
|
3985
4218
|
if (message.multipleSections) {
|
|
@@ -4037,47 +4270,59 @@ export function generateStudioBridgeScript(options) {
|
|
|
4037
4270
|
function init() {
|
|
4038
4271
|
const params = new URLSearchParams(window.location.search);
|
|
4039
4272
|
const studioEmbed = params.get('studio_embed') === 'true';
|
|
4273
|
+
const isStandalone = window.parent === window && !studioEmbed;
|
|
4040
4274
|
|
|
4041
|
-
if (
|
|
4042
|
-
|
|
4043
|
-
|
|
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
|
+
}
|
|
4044
4281
|
}
|
|
4045
4282
|
|
|
4046
4283
|
console.debug('[StudioBridge] Initializing...');
|
|
4047
4284
|
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
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
|
+
}
|
|
4051
4295
|
|
|
4052
|
-
setupConsoleCapture();
|
|
4053
|
-
setupErrorHandling();
|
|
4054
|
-
setupInspectMode();
|
|
4055
4296
|
setupMarkdownEditor(params);
|
|
4056
4297
|
|
|
4057
4298
|
window.addEventListener('message', handleStudioMessage);
|
|
4058
4299
|
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
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 {
|
|
4064
4310
|
notifyAppLoaded();
|
|
4065
4311
|
setupMutationObserver();
|
|
4066
|
-
}
|
|
4067
|
-
} else {
|
|
4068
|
-
notifyAppLoaded();
|
|
4069
|
-
setupMutationObserver();
|
|
4070
|
-
}
|
|
4312
|
+
}
|
|
4071
4313
|
|
|
4072
|
-
|
|
4314
|
+
window.addEventListener('beforeunload', notifyAppUnloaded);
|
|
4315
|
+
}
|
|
4073
4316
|
|
|
4074
4317
|
const colorMode = params.get('color_mode');
|
|
4075
4318
|
if (colorMode) setColorMode(colorMode);
|
|
4076
4319
|
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
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
|
+
}
|
|
4081
4326
|
}
|
|
4082
4327
|
|
|
4083
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
|
|
|
@@ -78,6 +82,14 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
78
82
|
let markdownLatestSelections = [];
|
|
79
83
|
let markdownHasUnsavedChanges = false;
|
|
80
84
|
let markdownSaveInProgress = false;
|
|
85
|
+
let markdownYDoc = null;
|
|
86
|
+
let markdownYProvider = null;
|
|
87
|
+
let markdownYText = null;
|
|
88
|
+
let markdownYjsConnected = false;
|
|
89
|
+
let markdownYjsSetupId = 0;
|
|
90
|
+
let markdownYjsY = null;
|
|
91
|
+
let markdownPendingSelection = null;
|
|
92
|
+
const LEXICAL_YJS_ORIGIN = 'lexical-yjs-binding';
|
|
81
93
|
|
|
82
94
|
const MARKDOWN_SLASH_COMMANDS = [
|
|
83
95
|
{
|
|
@@ -1418,6 +1430,247 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
1418
1430
|
}, 120);
|
|
1419
1431
|
}
|
|
1420
1432
|
|
|
1433
|
+
function computeTextDiff(oldText, newText) {
|
|
1434
|
+
var prefixLen = 0;
|
|
1435
|
+
var minLen = Math.min(oldText.length, newText.length);
|
|
1436
|
+
while (prefixLen < minLen && oldText.charCodeAt(prefixLen) === newText.charCodeAt(prefixLen)) {
|
|
1437
|
+
prefixLen++;
|
|
1438
|
+
}
|
|
1439
|
+
var suffixLen = 0;
|
|
1440
|
+
var maxSuffix = minLen - prefixLen;
|
|
1441
|
+
while (suffixLen < maxSuffix &&
|
|
1442
|
+
oldText.charCodeAt(oldText.length - 1 - suffixLen) === newText.charCodeAt(newText.length - 1 - suffixLen)) {
|
|
1443
|
+
suffixLen++;
|
|
1444
|
+
}
|
|
1445
|
+
return {
|
|
1446
|
+
index: prefixLen,
|
|
1447
|
+
deleteCount: oldText.length - prefixLen - suffixLen,
|
|
1448
|
+
insertText: newText.slice(prefixLen, suffixLen > 0 ? newText.length - suffixLen : undefined)
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function syncLocalChangeToYText(fullContent) {
|
|
1453
|
+
if (!markdownYText || !markdownYDoc) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
var currentYContent = markdownYText.toString();
|
|
1457
|
+
if (currentYContent === fullContent) {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
var diff = computeTextDiff(currentYContent, fullContent);
|
|
1461
|
+
if (diff.deleteCount === 0 && diff.insertText === '') {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
markdownYDoc.transact(function() {
|
|
1465
|
+
if (diff.deleteCount > 0) {
|
|
1466
|
+
markdownYText.delete(diff.index, diff.deleteCount);
|
|
1467
|
+
}
|
|
1468
|
+
if (diff.insertText) {
|
|
1469
|
+
markdownYText.insert(diff.index, diff.insertText);
|
|
1470
|
+
}
|
|
1471
|
+
}, LEXICAL_YJS_ORIGIN);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function setupMarkdownYjsConnection(config) {
|
|
1475
|
+
if (markdownYDoc) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
var setupId = ++markdownYjsSetupId;
|
|
1480
|
+
|
|
1481
|
+
Promise.all([
|
|
1482
|
+
import('https://esm.sh/yjs@13.6.28?target=es2022'),
|
|
1483
|
+
import('https://esm.sh/y-websocket@2.1.0?deps=yjs@13.6.28&target=es2022')
|
|
1484
|
+
]).then(function(modules) {
|
|
1485
|
+
// Abort if edit mode was closed while imports were loading
|
|
1486
|
+
if (setupId !== markdownYjsSetupId) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
var Y = modules[0];
|
|
1491
|
+
var WebsocketProvider = modules[1].WebsocketProvider;
|
|
1492
|
+
markdownYjsY = Y;
|
|
1493
|
+
|
|
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.
|
|
1497
|
+
var provider = new WebsocketProvider(config.wsUrl, config.guid, doc, {
|
|
1498
|
+
resyncInterval: -1
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
var ytext = doc.getText(config.fileId);
|
|
1502
|
+
|
|
1503
|
+
markdownYDoc = doc;
|
|
1504
|
+
markdownYProvider = provider;
|
|
1505
|
+
markdownYText = ytext;
|
|
1506
|
+
|
|
1507
|
+
// Filter non-binary messages to prevent y-websocket parse errors
|
|
1508
|
+
provider.on('status', function(event) {
|
|
1509
|
+
console.debug('[StudioBridge] Yjs status:', event.status);
|
|
1510
|
+
if (event.status === 'connected' && provider.ws) {
|
|
1511
|
+
var origOnMessage = provider.ws.onmessage;
|
|
1512
|
+
provider.ws.onmessage = function(wsEvent) {
|
|
1513
|
+
if (typeof wsEvent.data === 'string') {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (origOnMessage) {
|
|
1517
|
+
origOnMessage.call(provider.ws, wsEvent);
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
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
|
+
|
|
1610
|
+
provider.on('sync', function(synced) {
|
|
1611
|
+
if (synced && !markdownYjsConnected) {
|
|
1612
|
+
markdownYjsConnected = true;
|
|
1613
|
+
|
|
1614
|
+
var ytextContent = ytext.toString();
|
|
1615
|
+
if (markdownCurrentContent && markdownCurrentContent !== ytextContent) {
|
|
1616
|
+
// User typed before sync completed — push local edits to Y.Text
|
|
1617
|
+
syncLocalChangeToYText(markdownCurrentContent);
|
|
1618
|
+
} else if (ytextContent) {
|
|
1619
|
+
// No local edits — seed editor from Y.Text
|
|
1620
|
+
applyMarkdownContent(ytextContent);
|
|
1621
|
+
}
|
|
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
|
+
|
|
1635
|
+
// Observe Y.Text for remote changes (from other users / Monaco)
|
|
1636
|
+
ytext.observe(function(event) {
|
|
1637
|
+
if (event.transaction.origin === LEXICAL_YJS_ORIGIN) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
var fullContent = ytext.toString();
|
|
1641
|
+
if (fullContent === markdownCurrentContent) {
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
applyMarkdownContent(fullContent);
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
// Initial awareness sync after Yjs is connected
|
|
1648
|
+
syncAwareness();
|
|
1649
|
+
|
|
1650
|
+
console.debug('[StudioBridge] Yjs synced, bound to Y.Text for fileId:', config.fileId);
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
}).catch(function(error) {
|
|
1654
|
+
console.error('[StudioBridge] Failed to setup Yjs connection:', error);
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function disposeMarkdownYjs() {
|
|
1659
|
+
markdownYjsSetupId++;
|
|
1660
|
+
if (markdownYProvider) {
|
|
1661
|
+
markdownYProvider.disconnect();
|
|
1662
|
+
markdownYProvider.destroy();
|
|
1663
|
+
markdownYProvider = null;
|
|
1664
|
+
}
|
|
1665
|
+
if (markdownYDoc) {
|
|
1666
|
+
markdownYDoc.destroy();
|
|
1667
|
+
markdownYDoc = null;
|
|
1668
|
+
}
|
|
1669
|
+
markdownYText = null;
|
|
1670
|
+
markdownYjsConnected = false;
|
|
1671
|
+
markdownYjsY = null;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1421
1674
|
function getTextOffsetWithinRoot(root, targetNode, targetOffset) {
|
|
1422
1675
|
if (!root || !targetNode) {
|
|
1423
1676
|
return 0;
|
|
@@ -2503,27 +2756,26 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
2503
2756
|
const start = editorOffsetToSourceOffset(selection.start, 'start');
|
|
2504
2757
|
const end = editorOffsetToSourceOffset(selection.end, 'end');
|
|
2505
2758
|
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
+
}
|
|
2513
2772
|
}, 80);
|
|
2514
2773
|
}
|
|
2515
2774
|
|
|
2516
2775
|
function clearMarkdownSelectionSync() {
|
|
2517
|
-
if (
|
|
2518
|
-
|
|
2776
|
+
if (markdownYProvider) {
|
|
2777
|
+
markdownYProvider.awareness.setLocalStateField('selection', null);
|
|
2519
2778
|
}
|
|
2520
|
-
postToStudio({
|
|
2521
|
-
action: 'markdownSelectionChange',
|
|
2522
|
-
fileId: markdownFileId,
|
|
2523
|
-
filePath: PAGE_PATH,
|
|
2524
|
-
start: -1,
|
|
2525
|
-
end: -1
|
|
2526
|
-
});
|
|
2527
2779
|
}
|
|
2528
2780
|
|
|
2529
2781
|
function clearMarkdownSelectionOverlay() {
|
|
@@ -2917,6 +3169,9 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
2917
3169
|
}
|
|
2918
3170
|
markdownCurrentContent = fullContent;
|
|
2919
3171
|
markdownHasUnsavedChanges = true;
|
|
3172
|
+
if (markdownYjsConnected) {
|
|
3173
|
+
syncLocalChangeToYText(fullContent);
|
|
3174
|
+
}
|
|
2920
3175
|
scheduleMarkdownSync(fullContent);
|
|
2921
3176
|
scheduleMarkdownSelectionOverlayRender();
|
|
2922
3177
|
}
|
|
@@ -3078,7 +3333,7 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
3078
3333
|
return;
|
|
3079
3334
|
}
|
|
3080
3335
|
|
|
3081
|
-
if (markdownLexicalApi &&
|
|
3336
|
+
if (markdownLexicalApi && markdownLexicalRenderedContent === content) {
|
|
3082
3337
|
console.debug('[StudioBridge] applyMarkdownContent: skipped (content unchanged)');
|
|
3083
3338
|
markdownCurrentContent = content;
|
|
3084
3339
|
scheduleMarkdownSelectionOverlayRender();
|
|
@@ -3708,6 +3963,15 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
3708
3963
|
scheduleMarkdownSlashMenuUpdate();
|
|
3709
3964
|
scheduleMarkdownInlineToolbarUpdate();
|
|
3710
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
|
+
}
|
|
3711
3975
|
} else {
|
|
3712
3976
|
markdownBody.style.display = '';
|
|
3713
3977
|
if (markdownEditorRoot) {
|
|
@@ -3719,6 +3983,7 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
3719
3983
|
markdownOverlaySelections = [];
|
|
3720
3984
|
clearMarkdownSelectionOverlay();
|
|
3721
3985
|
clearMarkdownSelectionSync();
|
|
3986
|
+
disposeMarkdownYjs();
|
|
3722
3987
|
}
|
|
3723
3988
|
|
|
3724
3989
|
const nextUrl = new URL(window.location.href);
|
|
@@ -3942,16 +4207,6 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
3942
4207
|
if (!inspectMode) showHoverOverlay(message.id);
|
|
3943
4208
|
return;
|
|
3944
4209
|
|
|
3945
|
-
case 'setMarkdownContent':
|
|
3946
|
-
if (!isMarkdownPage()) {
|
|
3947
|
-
return;
|
|
3948
|
-
}
|
|
3949
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
3950
|
-
return;
|
|
3951
|
-
}
|
|
3952
|
-
applyMarkdownContent(message.content || '');
|
|
3953
|
-
return;
|
|
3954
|
-
|
|
3955
4210
|
case 'setMarkdownPersistState':
|
|
3956
4211
|
if (!isMarkdownPage()) {
|
|
3957
4212
|
return;
|
|
@@ -3968,26 +4223,6 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
3968
4223
|
}
|
|
3969
4224
|
return;
|
|
3970
4225
|
|
|
3971
|
-
case 'setMarkdownPresence':
|
|
3972
|
-
if (!isMarkdownPage()) {
|
|
3973
|
-
return;
|
|
3974
|
-
}
|
|
3975
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
3976
|
-
return;
|
|
3977
|
-
}
|
|
3978
|
-
setMarkdownPresence(message.users);
|
|
3979
|
-
return;
|
|
3980
|
-
|
|
3981
|
-
case 'setMarkdownSelections':
|
|
3982
|
-
if (!isMarkdownPage()) {
|
|
3983
|
-
return;
|
|
3984
|
-
}
|
|
3985
|
-
if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
|
|
3986
|
-
return;
|
|
3987
|
-
}
|
|
3988
|
-
setMarkdownSelections(message.selections);
|
|
3989
|
-
return;
|
|
3990
|
-
|
|
3991
4226
|
case 'screenshot':
|
|
3992
4227
|
(async function() {
|
|
3993
4228
|
if (message.multipleSections) {
|
|
@@ -4045,47 +4280,59 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
|
|
|
4045
4280
|
function init() {
|
|
4046
4281
|
const params = new URLSearchParams(window.location.search);
|
|
4047
4282
|
const studioEmbed = params.get('studio_embed') === 'true';
|
|
4283
|
+
const isStandalone = window.parent === window && !studioEmbed;
|
|
4048
4284
|
|
|
4049
|
-
if (
|
|
4050
|
-
|
|
4051
|
-
|
|
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
|
+
}
|
|
4052
4291
|
}
|
|
4053
4292
|
|
|
4054
4293
|
console.debug('[StudioBridge] Initializing...');
|
|
4055
4294
|
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
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
|
+
}
|
|
4059
4305
|
|
|
4060
|
-
setupConsoleCapture();
|
|
4061
|
-
setupErrorHandling();
|
|
4062
|
-
setupInspectMode();
|
|
4063
4306
|
setupMarkdownEditor(params);
|
|
4064
4307
|
|
|
4065
4308
|
window.addEventListener('message', handleStudioMessage);
|
|
4066
4309
|
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
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 {
|
|
4072
4320
|
notifyAppLoaded();
|
|
4073
4321
|
setupMutationObserver();
|
|
4074
|
-
}
|
|
4075
|
-
} else {
|
|
4076
|
-
notifyAppLoaded();
|
|
4077
|
-
setupMutationObserver();
|
|
4078
|
-
}
|
|
4322
|
+
}
|
|
4079
4323
|
|
|
4080
|
-
|
|
4324
|
+
window.addEventListener('beforeunload', notifyAppUnloaded);
|
|
4325
|
+
}
|
|
4081
4326
|
|
|
4082
4327
|
const colorMode = params.get('color_mode');
|
|
4083
4328
|
if (colorMode) setColorMode(colorMode);
|
|
4084
4329
|
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
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
|
+
}
|
|
4089
4336
|
}
|
|
4090
4337
|
|
|
4091
4338
|
console.debug('[StudioBridge] Initialized successfully');
|