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
42 changes: 42 additions & 0 deletions .github/workflows/figma-transformer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Figma Transformer

on:
pull_request:
paths:
- ".github/workflows/figma-transformer.yml"
- "figma-transformer/**"
push:
branches:
- trunk
paths:
- ".github/workflows/figma-transformer.yml"
- "figma-transformer/**"

jobs:
validate:
name: Composer validate and test
runs-on: ubuntu-latest

defaults:
run:
working-directory: figma-transformer

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: "8.1"
coverage: none
tools: composer:v2

- name: Install dependencies
run: composer install --no-interaction --prefer-dist --no-progress

- name: Validate Composer package metadata
run: composer validate --strict

- name: Run package tests
run: composer test
34 changes: 34 additions & 0 deletions .github/workflows/php-transformer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ on:
paths:
- ".github/workflows/php-transformer.yml"
- "php-transformer/**"
- "scripts/production-acceptance-matrix.php"
- "tests/contract/production-acceptance-matrix.php"
push:
branches:
- trunk
paths:
- ".github/workflows/php-transformer.yml"
- "php-transformer/**"
- "scripts/production-acceptance-matrix.php"
- "tests/contract/production-acceptance-matrix.php"

jobs:
validate:
Expand Down Expand Up @@ -41,6 +45,36 @@ jobs:
- name: Run package tests
run: composer test

- name: Run repository acceptance matrix contract
working-directory: .
run: php tests/contract/production-acceptance-matrix.php

visual-parity:
name: Generic visual parity tools
runs-on: ubuntu-latest

defaults:
run:
working-directory: php-transformer/tools/visual-parity

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install dependencies
run: npm ci

- name: Install Chromium
run: npx playwright install --with-deps chromium

- name: Run visual parity tests
run: npm test

wordpress-site-plan:
name: WordPress site plan integration
runs-on: ubuntu-24.04
Expand Down
4 changes: 3 additions & 1 deletion figma-transformer/scripts/figma-fixture-matrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,9 @@ function matrix_dom_box_capture_command(string $homeboyCommand, string $domBoxPr
return $command;
}

$environment = 'HOMEBOY_DOM_BOX_CAPTURE_COMMAND=' . escapeshellarg($domBoxProviderCommand);
$environment = 'HOMEBOY_DOM_BOX_CAPTURE_COMMAND=' . escapeshellarg($domBoxProviderCommand)
. ' HOMEBOY_DOM_BOX_NODE_ID_ATTR=' . escapeshellarg('data-figma-node-id')
. ' HOMEBOY_DOM_BOX_NODE_NAME_ATTR=' . escapeshellarg('data-figma-node-name,data-figma-name');
if ( ! empty($captureTargets) ) {
$environment .= ' HOMEBOY_DOM_BOX_CAPTURE_TARGETS_JSON=' . escapeshellarg((string) json_encode($captureTargets, JSON_UNESCAPED_SLASHES));
}
Expand Down
2 changes: 2 additions & 0 deletions figma-transformer/tests/contract/FixtureMatrixContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,8 @@ function blocks_engine_figma_transformer_run_fixture_matrix_contract(callable $a
$matrixAliasCaptureCommand = (string) ($matrixAliasSummary['fixtures'][0]['dom_box_capture']['command'] ?? '');
$assert(str_contains($matrixAliasCaptureCommand, escapeshellarg('/opt/homeboy-alias')), 'fixture-matrix-alias-capture-uses-homeboy-bin');
$assert(str_contains($matrixAliasCaptureCommand, 'HOMEBOY_DOM_BOX_CAPTURE_COMMAND=' . escapeshellarg('node dom-box-alias')), 'fixture-matrix-alias-capture-uses-dom-box-command');
$assert(str_contains($matrixAliasCaptureCommand, 'HOMEBOY_DOM_BOX_NODE_ID_ATTR=' . escapeshellarg('data-figma-node-id')), 'fixture-matrix-supplies-figma-node-id-contract');
$assert(str_contains($matrixAliasCaptureCommand, 'HOMEBOY_DOM_BOX_NODE_NAME_ATTR=' . escapeshellarg('data-figma-node-name,data-figma-name')), 'fixture-matrix-supplies-figma-node-name-contract');

$assert(is_array($matrixCanonicalSummary), 'fixture-matrix-canonical-json-summary');
$assert('/opt/homeboy-canonical' === ($matrixCanonicalSummary['homeboy_command'] ?? null), 'fixture-matrix-homeboy-command-canonical');
Expand Down
2 changes: 1 addition & 1 deletion figma-transformer/tests/contract/KiwiParserContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function blocks_engine_figma_transformer_run_kiwi_parser_contract(callable $asse
. pack('V', 106)
. blocks_engine_figma_transformer_kiwi_chunk("\x28\xb5\x2f\xfd" . 'adapter-frame')
);
$failingAdapterResult = ( new ZstdCapability(static fn (): false => false) )->uncompress("\x28\xb5\x2f\xfd" . 'adapter-frame', 'ContractTest', 3);
$failingAdapterResult = ( new ZstdCapability(static fn (): bool => false) )->uncompress("\x28\xb5\x2f\xfd" . 'adapter-frame', 'ContractTest', 3);
$commandAdapter = new ZstdCapability(new ZstdCommandDecoder(array(PHP_BINARY, '-r', '$payload = stream_get_contents(STDIN); fwrite(STDOUT, $payload);')));
$commandAdapterResult = $commandAdapter->uncompress('command adapter bytes', 'ContractTest', 4);
$limitedCommandAdapterResult = $commandAdapter->uncompress('command adapter bytes', 'ContractTest', 5, array('max_decoded_bytes' => 1));
Expand Down
1 change: 0 additions & 1 deletion php-transformer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"php tests/contract/run.php",
"php tests/contract/finding-contract.php",
"php tests/contract/wordpress-site-plan.php",
"php tests/contract/production-acceptance-matrix.php",
"@test:unit"
],
"test:unit": [
Expand Down
21 changes: 17 additions & 4 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5894,7 +5894,7 @@ private function searchBlockFromForm(DOMElement $form): ?array
*/
private function searchBlockFromStandaloneControl(DOMElement $element): ?array
{
if ( 0 < $element->getElementsByTagName('form')->length || 0 < $element->getElementsByTagName('script')->length || array() !== $this->eventMetadata($element) ) {
if ( 0 < $element->getElementsByTagName('form')->length || 0 < $element->getElementsByTagName('script')->length || array() !== $this->eventMetadata($element) || $this->isRuntimeDomTarget($element) ) {
return null;
}

Expand All @@ -5904,10 +5904,9 @@ private function searchBlockFromStandaloneControl(DOMElement $element): ?array
$inputs[] = $input;
}
}
if ( 1 !== count($inputs) || array() !== $this->eventMetadata($inputs[0]) ) {
if ( 1 !== count($inputs) || array() !== $this->eventMetadata($inputs[0]) || $this->isRuntimeDomTarget($inputs[0]) ) {
return null;
}

$controls = $this->formControlElements($element);
if ( 1 !== count($controls) ) {
return null;
Expand All @@ -5925,7 +5924,21 @@ private function searchBlockFromStandaloneControl(DOMElement $element): ?array
if ( '' === $label ) {
$label = $this->attr($searchInput, 'placeholder');
}
return $this->htmlPreservationBlock($element);

if ( '' !== $this->attr($searchInput, 'id') || 's' !== $this->attr($searchInput, 'name') ) {
return $this->htmlPreservationBlock($element);
}
if ( 1 !== $this->childElementCount($element) ) {
return null;
}

$placeholder = $this->attr($searchInput, 'placeholder');
return $this->createBlock('core/search', array_merge($this->presentationAttributes($element), array(
'label' => '' !== $label ? $label : 'Search',
'showLabel' => false,
'placeholder' => $placeholder,
'buttonPosition' => 'no-button',
)), array(), $element);
}

/**
Expand Down
17 changes: 17 additions & 0 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,23 @@ public function match(DOMElement $element, PatternContext $context): ?array
$assert('select' === ($standaloneControls['source_reports']['runtime_islands'][0]['control']['tag'] ?? ''), 'runtime-targeted standalone control reports control metadata');
$assert(str_contains((string) ($standaloneControls['source_reports']['runtime_islands'][0]['source_snippet'] ?? ''), '<select class="js-sort-select"'), 'runtime-targeted standalone control preserves source snippet metadata');

$standaloneSearch = ( new HtmlTransformer() )->transform(
'<div class="site-search"><input type="search" name="s" placeholder="Search articles" aria-label="Search articles"></div>'
)->toArray();
$standaloneSearchBlock = $standaloneSearch['blocks'][0] ?? array();
$assert('core/search' === ($standaloneSearchBlock['blockName'] ?? ''), 'script-free standalone search input converts to core/search');
$assert('Search articles' === ($standaloneSearchBlock['attrs']['placeholder'] ?? ''), 'standalone core/search preserves the source placeholder');
$assert('no-button' === ($standaloneSearchBlock['attrs']['buttonPosition'] ?? ''), 'standalone input-only search keeps the no-button presentation');
$assert(! str_contains((string) ($standaloneSearch['serialized_blocks'] ?? ''), '<!-- wp:html'), 'standalone search input avoids core/html');

$runtimeDescendantSearch = ( new HtmlTransformer() )->transform(
'<div class="site-search"><input type="search" name="s" placeholder="Search"><span id="search-status" aria-live="polite"></span></div>',
array('runtime_dom_selectors' => array('#search-status'))
)->toArray();
$assert(! str_contains((string) ($runtimeDescendantSearch['serialized_blocks'] ?? ''), '<!-- wp:search'), 'synthetic search with an additional runtime descendant is not collapsed to core/search');
$assert(str_contains((string) ($runtimeDescendantSearch['serialized_blocks'] ?? ''), 'search-status'), 'synthetic search preserves an additional runtime descendant');
$assert(1 === count($runtimeDescendantSearch['source_reports']['runtime_islands'] ?? array()), 'synthetic search reports its preserved runtime descendant');

$labelWrappedRuntimeControls = ( new HtmlTransformer() )->transform(
'<main><label class="tool"><span>Theme</span><select id="scheme-select"><option>Harbor</option></select></label><label class="tool"><input type="checkbox" id="crt-toggle"><span>CRT</span></label></main>',
array('runtime_dom_selectors' => array('#scheme-select', '#crt-toggle'))
Expand Down
30 changes: 4 additions & 26 deletions php-transformer/tools/visual-parity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,7 @@ homeboy tunnel artifact-origin dom-boxes \
--report=<report.json>
```

Use the generated `.fig -> HTML` artifact directory as `<artifact-root>`. The directory must contain `index.html` and any referenced CSS/assets. Keep committed docs and PR descriptions on placeholders such as `<fisiostetic-html-artifact-root>`, `<fse-html-artifact-root>`, and `<tt5-html-artifact-root>`; put machine-local paths only in local operator notes.

Example artifact captures:

```sh
HOMEBOY_DOM_BOX_CAPTURE_COMMAND='node php-transformer/tools/visual-parity/bin/dom-box-provider.mjs' \
homeboy tunnel artifact-origin dom-boxes \
--root=<fisiostetic-html-artifact-root> \
--entrypoint=index.html \
--report=<fisiostetic-dom-box-report.json>

HOMEBOY_DOM_BOX_CAPTURE_COMMAND='node php-transformer/tools/visual-parity/bin/dom-box-provider.mjs' \
homeboy tunnel artifact-origin dom-boxes \
--root=<fse-html-artifact-root> \
--entrypoint=index.html \
--report=<fse-dom-box-report.json>

HOMEBOY_DOM_BOX_CAPTURE_COMMAND='node php-transformer/tools/visual-parity/bin/dom-box-provider.mjs' \
homeboy tunnel artifact-origin dom-boxes \
--root=<tt5-html-artifact-root> \
--entrypoint=index.html \
--report=<tt5-dom-box-report.json>
```
The artifact directory must contain `index.html` and any referenced CSS/assets. Keep committed docs and PR descriptions on placeholders such as `<artifact-root>`; put machine-local paths only in local operator notes.

The report is repeatable when the artifact root, entrypoint, browser version, viewport defaults, and node identity attributes stay fixed. Attach the JSON report to the Homeboy run, issue, or PR evidence surface so the next operator can compare generated HTML structure and positions without re-running the full transform.

Expand All @@ -104,10 +82,10 @@ node bin/dom-box-provider.mjs --node-id-attr=data-node-id --node-name-attr=data-

| Setting | Env var | Flag | Default |
| --- | --- | --- | --- |
| Node id attribute | `HOMEBOY_DOM_BOX_NODE_ID_ATTR` | `--node-id-attr` | `data-figma-node-id` |
| Node name attributes | `HOMEBOY_DOM_BOX_NODE_NAME_ATTR` | `--node-name-attr` | `data-figma-node-name,data-figma-name` |
| Node id attribute | `HOMEBOY_DOM_BOX_NODE_ID_ATTR` | `--node-id-attr` | `data-node-id` |
| Node name attributes | `HOMEBOY_DOM_BOX_NODE_NAME_ATTR` | `--node-name-attr` | `data-node-name` |

The node id attribute drives element enumeration, selector generation, and id reads. Node name attributes are tried in order; `aria-label` is always appended as a final generic fallback. The defaults stay backward-compatible with the figma-transformer's `data-figma-*` output, so existing figma callers work with no changes; non-figma consumers override the attributes to match their own emitter.
The node id attribute drives element enumeration, selector generation, and id reads. Node name attributes are tried in order; `aria-label` is always appended as a final generic fallback. Callers with different emitter contracts supply their own attributes explicitly.

## Screenshot provider

Expand Down
6 changes: 3 additions & 3 deletions php-transformer/tools/visual-parity/bin/dom-box-provider.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env node

const DEFAULT_VIEWPORT = { width: 1440, height: 900, device_scale_factor: 1 };
const DEFAULT_NODE_ID_ATTR = 'data-figma-node-id';
const DEFAULT_NODE_NAME_ATTRS = ['data-figma-node-name', 'data-figma-name'];
const DEFAULT_NODE_ID_ATTR = 'data-node-id';
const DEFAULT_NODE_NAME_ATTRS = ['data-node-name'];
const STATIC_ARTIFACT_CAPTURE_IGNORE_ATTR = 'data-static-artifact-capture';
const SOURCE_NODE_TYPE_ATTR = 'data-source-node-type';
const SOURCE_VISUAL_WIDTH_ATTR = 'data-source-visual-width';
Expand Down Expand Up @@ -265,7 +265,7 @@ function targetsForPage(targets, pagePath) {
}

function printHelp() {
process.stdout.write(`Capture DOM boxes for Homeboy artifact-origin dom-boxes.\n\nNode identity is keyed off a configurable attribute so the tool is product-neutral.\nThe figma-transformer's data-figma-* attributes remain the backward-compatible default.\n\nEnvironment:\n HOMEBOY_DOM_BOX_BASE_URL Static artifact origin base URL.\n HOMEBOY_DOM_BOX_PAGE_PATHS_JSON JSON array of page paths to capture.\n HOMEBOY_DOM_BOX_TEXT_SAMPLE_LIMIT Optional positive integer, default 160.\n HOMEBOY_DOM_BOX_NODE_ID_ATTR Node identity attribute, default ${DEFAULT_NODE_ID_ATTR}.\n HOMEBOY_DOM_BOX_NODE_NAME_ATTR Comma-separated node name attributes, default ${DEFAULT_NODE_NAME_ATTRS.join(',')} (aria-label is always a final fallback).\n HOMEBOY_DOM_BOX_CAPTURE_TARGETS_JSON Optional page/source-frame viewport capture targets.\n\nFlags (override the matching environment variable):\n --node-id-attr=<attr> Node identity attribute used for enumeration, selectors, and id reads.\n --node-name-attr=<attr>[,<attr>...] Node name attributes, tried in order before aria-label.\n --preflight Verify Playwright and Chromium are installed, then exit.\n\nOutput:\n JSON browser payload on stdout for Homeboy to shape as homeboy/static-artifact-dom-boxes/v1.\n`);
process.stdout.write(`Capture DOM boxes for Homeboy artifact-origin dom-boxes.\n\nNode identity is keyed off configurable, format-neutral attributes.\n\nEnvironment:\n HOMEBOY_DOM_BOX_BASE_URL Static artifact origin base URL.\n HOMEBOY_DOM_BOX_PAGE_PATHS_JSON JSON array of page paths to capture.\n HOMEBOY_DOM_BOX_TEXT_SAMPLE_LIMIT Optional positive integer, default 160.\n HOMEBOY_DOM_BOX_NODE_ID_ATTR Node identity attribute, default ${DEFAULT_NODE_ID_ATTR}.\n HOMEBOY_DOM_BOX_NODE_NAME_ATTR Comma-separated node name attributes, default ${DEFAULT_NODE_NAME_ATTRS.join(',')} (aria-label is always a final fallback).\n HOMEBOY_DOM_BOX_CAPTURE_TARGETS_JSON Optional page/source-frame viewport capture targets.\n\nFlags (override the matching environment variable):\n --node-id-attr=<attr> Node identity attribute used for enumeration, selectors, and id reads.\n --node-name-attr=<attr>[,<attr>...] Node name attributes, tried in order before aria-label.\n --preflight Verify Playwright and Chromium are installed, then exit.\n\nOutput:\n JSON browser payload on stdout for Homeboy to shape as homeboy/static-artifact-dom-boxes/v1.\n`);
}

async function extractElements(page, pagePath, textSampleLimit, nodeIdAttr, nodeNameAttrs) {
Expand Down
Loading
Loading