Skip to content
Merged
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
1 change: 1 addition & 0 deletions figma-transformer/src/Html/HtmlArtifactAssembler.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function baseCssRules(bool $renderTextGlyphPaths): array
'body{margin:0}',
'.figma-root{position:relative;width:100%;display:flex;flex-direction:column;align-items:center}',
'p,h1,h2,h3,h4,h5,h6{margin:0}',
'blockquote{margin:0}',
'ul,ol{margin:0;padding:0;list-style:none}',
'img{display:block;max-width:100%;height:auto}',
'a.figma-link{display:contents;color:inherit;text-decoration:inherit}',
Expand Down
2 changes: 1 addition & 1 deletion php-transformer/tools/visual-parity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"scripts": {
"install:browsers": "playwright install chromium",
"test": "node tests/smoke.mjs"
"test": "node tests/smoke.mjs && node tests/blockquote-margin-reset.mjs"
},
"devDependencies": {
"playwright": "^1.56.0"
Expand Down
115 changes: 115 additions & 0 deletions php-transformer/tools/visual-parity/tests/blockquote-margin-reset.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { createServer } from 'node:http';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';

const toolRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const repositoryRoot = path.resolve(toolRoot, '../../..');
const fixturePath = path.join(toolRoot, 'tests/fixtures/blockquote-margin-reset.scenegraph.json');
const outputDir = await mkdtemp(path.join(tmpdir(), 'blocks-engine-blockquote-margin-'));
const transformScript = `
require $argv[1];
$scenegraph = json_decode(file_get_contents($argv[2]), true, 512, JSON_THROW_ON_ERROR);
$result = blocks_engine_figma_transformer_transform_scenegraph($scenegraph);
foreach ($result['files'] as $file) {
$path = $argv[3] . '/' . $file['path'];
$directory = dirname($path);
if (!is_dir($directory)) {
mkdir($directory, 0777, true);
}
file_put_contents($path, $file['content']);
}
`;

const transform = spawnSync('php', [
'-r',
transformScript,
path.join(repositoryRoot, 'figma-transformer/figma-transformer.php'),
fixturePath,
outputDir,
], { encoding: 'utf8' });

if (transform.status !== 0) {
throw new Error(`Figma fixture transform failed:\n${transform.stderr}`);
}

const server = createServer(async (request, response) => {
const pathname = new URL(request.url, 'http://127.0.0.1').pathname;
const filename = pathname === '/' ? 'index.html' : pathname.slice(1);
if (!/^[A-Za-z0-9._/-]+$/.test(filename) || filename.includes('..')) {
response.writeHead(400).end();
return;
}

try {
const content = await readFile(path.join(outputDir, filename));
response.writeHead(200, { 'content-type': filename.endsWith('.css') ? 'text/css' : 'text/html' });
response.end(content);
} catch {
response.writeHead(404).end();
}
});

await new Promise((resolve, reject) => {
server.on('error', reject);
server.listen(0, '127.0.0.1', resolve);
});

try {
const address = server.address();
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 800, height: 600 } });
await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: 'load' });
const geometry = await page.evaluate(() => {
const preceding = document.querySelector('[data-figma-node-id="blockquote-margin:preceding"]');
const quote = document.querySelector('[data-figma-node-id="blockquote-margin:quote"]');
const following = document.querySelector('[data-figma-node-id="blockquote-margin:following"]');
if (!preceding || !quote || !following) {
throw new Error('Generated stack nodes are missing.');
}

const rect = (element) => element.getBoundingClientRect();
const computed = getComputedStyle(quote);
return {
tag: quote.tagName,
margins: {
top: computed.marginTop,
right: computed.marginRight,
bottom: computed.marginBottom,
left: computed.marginLeft,
},
preceding: rect(preceding).toJSON(),
quote: rect(quote).toJSON(),
following: rect(following).toJSON(),
};
});

assert(geometry.tag === 'BLOCKQUOTE', 'the generated quote uses a blockquote element');
assert(geometry.margins.top === '8px', 'the generated blockquote class keeps its explicit 8px source margin over the reset');
assert(geometry.margins.right === '0px' && geometry.margins.bottom === '0px' && geometry.margins.left === '0px', 'the reset removes Chromium blockquote UA 40px horizontal and 16px vertical margins');
assert(closeTo(geometry.quote.left, geometry.preceding.left), 'the blockquote has no UA horizontal offset');
assert(closeTo(geometry.quote.top - geometry.preceding.bottom, 24), 'the quote position combines the 16px stack gap and generated 8px margin');
assert(closeTo(geometry.following.top - geometry.quote.bottom, 16), 'the following sibling remains at the generated 16px stack gap');
} finally {
await browser.close();
}
} finally {
await new Promise((resolve) => server.close(resolve));
await rm(outputDir, { recursive: true, force: true });
}

console.log('Blockquote margin reset geometry test passed.');

function closeTo(actual, expected) {
return Math.abs(actual - expected) < 0.01;
}

function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "Blockquote Margin Reset Geometry Fixture",
"nodes": [
{
"id": "blockquote-margin:root",
"type": "FRAME",
"name": "Quote Stack",
"width": 480,
"height": 168,
"absoluteBoundingBox": { "x": 0, "y": 0, "width": 480, "height": 168 },
"layoutMode": "VERTICAL",
"itemSpacing": 16,
"children": [
{
"id": "blockquote-margin:preceding",
"type": "TEXT",
"name": "Body",
"characters": "Preceding sibling",
"x": 0,
"y": 0,
"width": 480,
"height": 24,
"absoluteBoundingBox": { "x": 0, "y": 0, "width": 480, "height": 24 },
"fontSize": 16
},
{
"id": "blockquote-margin:quote",
"type": "FRAME",
"name": "Blockquote",
"x": 0,
"y": 48,
"width": 480,
"height": 64,
"absoluteBoundingBox": { "x": 0, "y": 48, "width": 480, "height": 64 },
"children": [
{
"id": "blockquote-margin:quote-text",
"type": "TEXT",
"name": "Quote",
"characters": "The source adds an explicit eight pixel offset after the stack gap.",
"width": 480,
"height": 24,
"absoluteBoundingBox": { "x": 0, "y": 48, "width": 480, "height": 24 },
"fontSize": 16
}
]
},
{
"id": "blockquote-margin:following",
"type": "TEXT",
"name": "Body",
"characters": "Following sibling",
"x": 0,
"y": 128,
"width": 480,
"height": 24,
"absoluteBoundingBox": { "x": 0, "y": 128, "width": 480, "height": 24 },
"fontSize": 16
}
]
}
]
}
Loading