Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/__test__/html-preview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest';
import { applyHtmlPreviewColorMode } from '../html-preview';

describe('applyHtmlPreviewColorMode', () => {
it('leaves light mode previews unchanged', () => {
const srcDoc = '<h1>Hello</h1>';

expect(applyHtmlPreviewColorMode(srcDoc, 'light')).toBe(srcDoc);
});

it('adds dark preview styles to document fragments', () => {
const srcDoc = '<h1>Hello</h1>';
const themedSrcDoc = applyHtmlPreviewColorMode(srcDoc, 'dark');

expect(themedSrcDoc).toContain('data-php-playground-preview-theme');
expect(themedSrcDoc).toContain('background-color: rgb(30, 30, 30)');
expect(themedSrcDoc).toContain('color: #d4d4d4');
expect(themedSrcDoc.endsWith(srcDoc)).toBe(true);
});

it('injects dark preview styles inside the head when present', () => {
const srcDoc =
'<!doctype html><html><head><title>x</title></head><body>x</body></html>';
const themedSrcDoc = applyHtmlPreviewColorMode(srcDoc, 'dark');

expect(themedSrcDoc).toContain(
'<head><style data-php-playground-preview-theme>'
);
expect(themedSrcDoc.startsWith('<!doctype html>')).toBe(true);
});

it('does not inject the dark preview styles twice', () => {
const srcDoc = applyHtmlPreviewColorMode('<h1>Hello</h1>', 'dark');

expect(applyHtmlPreviewColorMode(srcDoc, 'dark')).toBe(srcDoc);
});
});
9 changes: 7 additions & 2 deletions src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as React from 'react';
import MonacoEditor, { type OnChange } from '@monaco-editor/react';
import { Format } from './format';
import debounce from 'debounce';
import { applyHtmlPreviewColorMode } from './html-preview';

function LoadSpinner() {
return (
Expand Down Expand Up @@ -54,6 +55,7 @@ function PhpPreview(params: { version: Version; format: Format }) {
const { files, activeFile } = sandpack;
const code = files[activeFile].code;
const [loading, result] = usePHP(params.version, code);
const { colorMode } = useColorMode();

// Generate a unique key whenever result changes (O(1) operation)
const iframeKey = React.useRef(0);
Expand Down Expand Up @@ -86,7 +88,7 @@ function PhpPreview(params: { version: Version; format: Format }) {
return (
<iframe
key={iframeKey.current}
srcDoc={result}
srcDoc={applyHtmlPreviewColorMode(result, colorMode)}
height="100%"
width="100%"
sandbox=""
Expand All @@ -104,6 +106,8 @@ function PhpCodeCallback(params: { onChangeCode: (code: string) => void }) {
}

function EditorLayout(params: { Editor: ReactElement; Preview: ReactElement }) {
const { colorMode } = useColorMode();

return (
<Flex direction="column" padding="3" bg="gray.800" height="100%">
<Flex
Expand Down Expand Up @@ -134,7 +138,8 @@ function EditorLayout(params: { Editor: ReactElement; Preview: ReactElement }) {
height={{ base: '50%', lg: '100%' }}
width={{ base: '100%', lg: '50%' }}
style={{
backgroundColor: 'white',
backgroundColor:
colorMode === 'light' ? 'white' : 'rgb(30, 30, 30)',
}}
>
{params.Preview}
Expand Down
52 changes: 52 additions & 0 deletions src/html-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
type PreviewColorMode = 'light' | 'dark';

const darkHtmlPreviewStyle = `<style data-php-playground-preview-theme>
html {
border: 1px solid #efefef;
border-bottom: 2px solid #efefef;
border-radius: 4px;
height: 99.5%;
background-color: rgb(30, 30, 30);
color: #d4d4d4;
}
</style>`;

export function applyHtmlPreviewColorMode(
srcDoc: string,
colorMode: PreviewColorMode
): string {
if (colorMode !== 'dark') {
return srcDoc;
}
if (srcDoc.includes('data-php-playground-preview-theme')) {
return srcDoc;
}
return insertPreviewStyle(srcDoc, darkHtmlPreviewStyle);
}

function insertPreviewStyle(srcDoc: string, style: string): string {
const headMatch = srcDoc.match(/<head(?:\s[^>]*)?>/i);
if (headMatch?.index !== undefined) {
return insertAt(srcDoc, headMatch.index + headMatch[0].length, style);
}

const htmlMatch = srcDoc.match(/<html(?:\s[^>]*)?>/i);
if (htmlMatch?.index !== undefined) {
return insertAt(srcDoc, htmlMatch.index + htmlMatch[0].length, style);
}

const doctypeMatch = srcDoc.match(/^\s*<!doctype[^>]*>/i);
if (doctypeMatch?.index !== undefined) {
return insertAt(
srcDoc,
doctypeMatch.index + doctypeMatch[0].length,
style
);
}

return `${style}${srcDoc}`;
}

function insertAt(value: string, index: number, insertion: string): string {
return `${value.slice(0, index)}${insertion}${value.slice(index)}`;
}