Skip to content

Commit 8f58ed1

Browse files
committed
fix(webview): load markdown styles in Firefox
1 parent 8a7bf87 commit 8f58ed1

4 files changed

Lines changed: 182 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Code v99.99.999
2626

2727
- `--idle-timeout-seconds` was only validated when passed as `--idle-timeout-seconds=<value>`;
2828
values of 60 or less passed as `--idle-timeout-seconds <value>` were silently accepted.
29+
- Load local styles configured with `markdown.styles` in Firefox 149 and later.
2930

3031
## [4.138.0](https://github.com/coder/code-server/releases/tag/v4.138.0) - 2026-09-19
3132

‎patches/csp-hashes.diff‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index
66
<meta charset="UTF-8">
77

88
<meta http-equiv="Content-Security-Policy"
9-
- content="default-src 'none'; script-src 'sha256-FFQoOVVa2tOE3uqUvirwaMNT20TZmrHcL2aaOjJ8BUo=' 'self'; frame-src 'self'; style-src 'unsafe-inline';">
10-
+ content="default-src 'none'; script-src 'sha256-24QqbpzqcqSyIGvg/ZwFRy5pPlhmVBa8CkUREEIUnBw=' 'self'; frame-src 'self'; style-src 'unsafe-inline';">
9+
- content="default-src 'none'; script-src 'sha256-FFQoOVVa2tOE3uqUvirwaMNT20TZmrHcL2aaOjJ8BUo=' 'self'; connect-src 'self' https://*.vscode-cdn.net; frame-src 'self'; style-src 'unsafe-inline';">
10+
+ content="default-src 'none'; script-src 'sha256-u3FINRB6N9ZtXZVeE1TdGILlPsfDesEgZJhO81bfwi0=' 'self'; connect-src 'self' https://*.vscode-cdn.net; frame-src 'self'; style-src 'unsafe-inline';">
1111

1212
<!-- Disable pinch zooming -->
1313
<meta name="viewport"

‎patches/webview.diff‎

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ request is not cross-origin.
1515
There is also an origin check we bypass (this seems to be related to how the
1616
webview host is separate by default but we serve on the same host).
1717

18-
To test, open a few types of webviews (images, markdown, extension details, etc).
18+
Firefox 149 and later skip service worker interception for cross-origin
19+
stylesheet requests. Load these stylesheets from the controlled outer webview
20+
frame and apply them to the content as constructed stylesheets.
21+
22+
To test, configure a local stylesheet with markdown.styles, then open a Markdown
23+
preview in Firefox and verify the custom stylesheet is applied. Also open a few
24+
other types of webviews (images, extension details, etc).
1925

2026
parentOriginHash changes
2127

@@ -53,6 +59,15 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index
5359
===================================================================
5460
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index.html
5561
+++ code-server/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index.html
62+
@@ -5,7 +5,7 @@
63+
<meta charset="UTF-8">
64+
65+
<meta http-equiv="Content-Security-Policy"
66+
- content="default-src 'none'; script-src 'sha256-FFQoOVVa2tOE3uqUvirwaMNT20TZmrHcL2aaOjJ8BUo=' 'self'; frame-src 'self'; style-src 'unsafe-inline';">
67+
+ content="default-src 'none'; script-src 'sha256-FFQoOVVa2tOE3uqUvirwaMNT20TZmrHcL2aaOjJ8BUo=' 'self'; connect-src 'self' https://*.vscode-cdn.net; frame-src 'self'; style-src 'unsafe-inline';">
68+
69+
<!-- Disable pinch zooming -->
70+
<meta name="viewport"
5671
@@ -253,7 +253,7 @@
5772
}
5873

@@ -75,6 +90,149 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index
7590
if (!crypto.subtle) {
7691
// cannot validate, not running in a secure context
7792
throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`);
93+
@@ -841,13 +847,69 @@
94+
}
95+
96+
/**
97+
- * @param {import('../webviewMessages').UpdateContentEvent} data
98+
- * @return {string}
99+
+ * Firefox 149+ skips service worker interception for cross-origin stylesheet
100+
+ * requests. Fetch these stylesheets from the controlled outer frame instead.
101+
+ *
102+
+ * @param {Document} contentDocument
103+
+ * @returns {Promise<Array<{ href: string, css: string, media: string, disabled: boolean }>>}
104+
*/
105+
- function toContentHtml(data) {
106+
+ async function loadFirefoxStylesheets(contentDocument) {
107+
+ if (!isFirefox) {
108+
+ return [];
109+
+ }
110+
+
111+
+ const resourceBaseAuthority = searchParams.get('vscode-resource-base-authority');
112+
+ if (!resourceBaseAuthority) {
113+
+ return [];
114+
+ }
115+
+
116+
+ const stylesheets = [];
117+
+ for (const link of contentDocument.querySelectorAll('link')) {
118+
+ if (!link.relList.contains('stylesheet')) {
119+
+ continue;
120+
+ }
121+
+
122+
+ let resourceUrl;
123+
+ try {
124+
+ resourceUrl = new URL(link.href, link.baseURI);
125+
+ } catch {
126+
+ continue;
127+
+ }
128+
+
129+
+ if (resourceUrl.protocol !== 'https:' || !resourceUrl.hostname.endsWith('.' + resourceBaseAuthority)) {
130+
+ continue;
131+
+ }
132+
+
133+
+ try {
134+
+ const response = await fetch(resourceUrl);
135+
+ if (!response.ok) {
136+
+ continue;
137+
+ }
138+
+
139+
+ stylesheets.push({
140+
+ href: resourceUrl.toString(),
141+
+ css: await response.text(),
142+
+ media: link.media,
143+
+ disabled: link.disabled,
144+
+ });
145+
+ link.remove();
146+
+ } catch {
147+
+ // Leave the link in place so the webview reports the original load failure.
148+
+ }
149+
+ }
150+
+
151+
+ return stylesheets;
152+
+ }
153+
+
154+
+ /**
155+
+ * @param {import('../webviewMessages').UpdateContentEvent} data
156+
+ * @returns {Promise<{ html: string, stylesheets: Array<{ href: string, css: string, media: string, disabled: boolean }> }>}
157+
+ */
158+
+ async function toContentHtml(data) {
159+
const options = data.options;
160+
const text = data.contents;
161+
const newDocument = new DOMParser().parseFromString(text, 'text/html');
162+
+ const stylesheets = await loadFirefoxStylesheets(newDocument);
163+
164+
newDocument.querySelectorAll('a').forEach(a => {
165+
if (!a.title) {
166+
@@ -904,7 +966,10 @@
167+
168+
// set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off
169+
// and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden
170+
- return '<!DOCTYPE html>\n' + newDocument.documentElement.outerHTML;
171+
+ return {
172+
+ html: '<!DOCTYPE html>\n' + newDocument.documentElement.outerHTML,
173+
+ stylesheets,
174+
+ };
175+
}
176+
177+
// Also forward events before the contents of the webview have loaded
178+
@@ -979,7 +1044,10 @@
179+
}
180+
181+
const options = data.options;
182+
- const newDocument = toContentHtml(data);
183+
+ const { html: newDocument, stylesheets } = await toContentHtml(data);
184+
+ if (currentUpdateId !== updateId) {
185+
+ return;
186+
+ }
187+
188+
const initialStyleVersion = styleVersion;
189+
190+
@@ -1055,15 +1123,44 @@
191+
/**
192+
* @param {Document} contentDocument
193+
*/
194+
+ async function applyFirefoxStylesheets(contentDocument) {
195+
+ if (!stylesheets.length || !contentDocument.defaultView) {
196+
+ return;
197+
+ }
198+
+
199+
+ /** @type {CSSStyleSheet[]} */
200+
+ const sheets = [];
201+
+ for (const stylesheet of stylesheets) {
202+
+ const sheet = new contentDocument.defaultView.CSSStyleSheet({
203+
+ baseURL: stylesheet.href,
204+
+ media: stylesheet.media,
205+
+ disabled: stylesheet.disabled,
206+
+ });
207+
+ try {
208+
+ await sheet.replace(stylesheet.css);
209+
+ } catch (error) {
210+
+ console.error(`Failed to apply stylesheet '${stylesheet.href}': ${error}`);
211+
+ continue;
212+
+ }
213+
+ sheets.push(sheet);
214+
+ }
215+
+
216+
+ contentDocument.adoptedStyleSheets = [...contentDocument.adoptedStyleSheets, ...sheets];
217+
+ }
218+
+
219+
+ /**
220+
+ * @param {Document} contentDocument
221+
+ */
222+
function onFrameLoaded(contentDocument) {
223+
perfMark('content/innerFrameLoaded')
224+
225+
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325
226+
- setTimeout(() => {
227+
+ setTimeout(async () => {
228+
contentDocument.open();
229+
contentDocument.write(newDocument);
230+
contentDocument.close();
231+
hookupOnLoadHandlers(newFrame);
232+
+ await applyFirefoxStylesheets(contentDocument);
233+
perfMark('content/wroteInnerContent')
234+
235+
if (initialStyleVersion !== styleVersion) {
78236
Index: code-server/lib/vscode/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
79237
===================================================================
80238
--- code-server.orig/lib/vscode/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html

‎test/e2e/webview.test.ts‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ import * as path from "path"
33
import { describe, test, expect } from "./baseFixture"
44

55
describe("Webviews", ["--disable-workspace-trust"], {}, () => {
6-
test("should preview a Markdown file", async ({ codeServerPage }) => {
7-
// Create Markdown file
6+
test("should preview a Markdown file with custom styles", async ({ codeServerPage }) => {
87
const heading = "Hello world"
98
const dir = await codeServerPage.workspaceDir
109
const file = path.join(dir, "text.md")
10+
const style = path.join(dir, "test.css")
11+
const settingsPath = path.join(dir, "User/settings.json")
12+
const settings = JSON.parse(await fs.readFile(settingsPath, "utf8"))
13+
14+
settings["markdown.styles"] = ["test.css"]
15+
await fs.writeFile(settingsPath, JSON.stringify(settings))
16+
await fs.writeFile(style, "h1 { color: rgb(1, 2, 3); }")
1117
await fs.writeFile(file, `# ${heading}`)
18+
19+
// Reload so the workbench reads the updated user settings before rendering
20+
// the Markdown preview.
21+
await codeServerPage.page.reload()
22+
await codeServerPage.reloadUntilEditorIsReady()
1223
await codeServerPage.openFile(file)
1324

1425
// Open Preview
@@ -18,8 +29,12 @@ describe("Webviews", ["--disable-workspace-trust"], {}, () => {
1829

1930
// It's an iframe within an iframe
2031
// so we have to do .frameLocator twice
21-
await expect(
22-
codeServerPage.page.frameLocator("iframe.webview.ready").frameLocator("#active-frame").getByText("Hello world"),
23-
).toBeVisible()
32+
const previewHeading = codeServerPage.page
33+
.frameLocator("iframe.webview.ready")
34+
.frameLocator("#active-frame")
35+
.getByText(heading)
36+
37+
await expect(previewHeading).toBeVisible()
38+
await expect(previewHeading).toHaveCSS("color", "rgb(1, 2, 3)")
2439
})
2540
})

0 commit comments

Comments
 (0)