Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/js-bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
version: 11.14.0

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
version: 11.14.0

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand All @@ -44,6 +44,12 @@ jobs:
- name: Build
run: pnpm build

- name: Install Chromium for SVG asset decode test
run: pnpm exec playwright install --with-deps chromium

- name: Run SVG asset decode test
run: pnpm test:svg-decode

- name: Run tests
run: pnpm test

Expand Down
7 changes: 5 additions & 2 deletions packages/blocks-engine/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@automattic/blocks-engine",
"version": "0.2.2",
"packageManager": "pnpm@11.14.0",
"description": "Convert HTML and static sites into WordPress block markup and block themes.",
"keywords": ["wordpress", "blocks", "gutenberg", "block-theme", "html", "static-site"],
"license": "GPL-3.0-or-later",
Expand Down Expand Up @@ -54,8 +55,9 @@
"scripts": {
"build": "tsup && node scripts/build-wp-runtime.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:bundle": "pnpm build && BLOCKS_ENGINE_WP_RUNTIME=bundle BLOCKS_ENGINE_WP_RUNTIME_PATH=\"$PWD/dist/wp-runtime.cjs\" vitest run",
"test": "vitest run --exclude src/__tests__/theme-svg-asset.test.ts",
"test:svg-decode": "vitest run src/__tests__/theme-svg-asset.test.ts",
"test:bundle": "pnpm build && BLOCKS_ENGINE_WP_RUNTIME=bundle BLOCKS_ENGINE_WP_RUNTIME_PATH=\"$PWD/dist/wp-runtime.cjs\" vitest run --exclude src/__tests__/theme-svg-asset.test.ts",
"bench": "node --import tsx scripts/tuner/bench.ts",
"bench:record": "node --import tsx scripts/tuner/bench.ts --record",
"bench:derive": "node --import tsx scripts/tuner/derive.ts",
Expand All @@ -73,6 +75,7 @@
"@wordpress/block-serialization-default-parser": "5.49.0",
"@wordpress/blocks": "15.22.0",
"esbuild": "^0.24.0",
"playwright": "1.61.1",
"tsup": "8.0.0",
"vitest": "2.0.0",
"typescript": "5.7.3",
Expand Down
41 changes: 41 additions & 0 deletions packages/blocks-engine/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
"iconAssets": [
{
"path": "assets/icon-0.svg",
"svg": "<svg fill=\"#ffffff\" viewBox=\"0 0 20 20\"><path d=\"M2 10h16v2H2z\"/></svg>"
"svg": "<svg fill=\"#ffffff\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M2 10h16v2H2z\"/></svg>"
}
]
}
Expand Down Expand Up @@ -191,7 +191,7 @@
"iconAssets": [
{
"path": "assets/icon-0.svg",
"svg": "<svg fill=\"#ffffff\" viewBox=\"0 0 20 20\"><path d=\"M2 10h16v2H2z\"/></svg>"
"svg": "<svg fill=\"#ffffff\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M2 10h16v2H2z\"/></svg>"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@
"iconAssets": [
{
"path": "assets/icon-0.svg",
"svg": "<svg fill=\"#ffffff\" viewBox=\"0 0 10 10\"><path d=\"M2 2h6v6H2z\"/></svg>"
"svg": "<svg fill=\"#ffffff\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 10\"><path d=\"M2 2h6v6H2z\"/></svg>"
}
]
},
Expand Down Expand Up @@ -349,7 +349,7 @@
"iconAssets": [
{
"path": "assets/icon-0.svg",
"svg": "<svg viewBox=\"0 0 10 10\"><path fill=\"#111\" stroke=\"#222\" d=\"M1 1h8v8H1z\"/></svg>"
"svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 10\"><path fill=\"#111\" stroke=\"#222\" d=\"M1 1h8v8H1z\"/></svg>"
}
]
},
Expand Down Expand Up @@ -621,7 +621,7 @@
"iconAssets": [
{
"path": "assets/icon-0.svg",
"svg": "<svg fill=\"#f8fafc\" viewBox=\"0 0 10 10\"><path d=\"M2 2h6v6H2z\"/></svg>"
"svg": "<svg fill=\"#f8fafc\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 10\"><path d=\"M2 2h6v6H2z\"/></svg>"
}
]
},
Expand Down Expand Up @@ -676,7 +676,7 @@
"iconAssets": [
{
"path": "assets/icon-0.svg",
"svg": "<svg viewBox=\"0 0 10 10\"><path d=\"M2 2h6v6H2z\"/></svg>"
"svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 10\"><path d=\"M2 2h6v6H2z\"/></svg>"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,20 +135,73 @@ describe('sanitizeSvgAsset DLA parity', () => {
sanitizeSvgAsset(
'<svg><script>alert(1)</script><foreignObject><p>x</p></foreignObject><path d="M3 9h4"/></svg>'
)
).toBe('<svg><path d="M3 9h4"/></svg>');
).toBe('<svg xmlns="http://www.w3.org/2000/svg"><path d="M3 9h4"/></svg>');

expect(
sanitizeSvgAsset(
'<svg><set attributeName="onload" to="alert(1)"/><animate attributeName="onbegin" to="x"></animate><path onload="evil()" onclick=\'evil2()\' d="M3 9h4"/></svg>'
)
).toBe('<svg><path d="M3 9h4"/></svg>');
).toBe('<svg xmlns="http://www.w3.org/2000/svg"><path d="M3 9h4"/></svg>');

expect(
sanitizeSvgAsset(
'<svg><image href="https://evil.example/x"/><a xlink:href="javascript:alert(1)">x</a><use href="#local-glyph"/><image href="data:image/png;base64,abc"/></svg>'
)
).toBe(
'<svg><image/><a xlink:href="alert(1)">x</a><use href="#local-glyph"/><image href="data:image/png;base64,abc"/></svg>'
'<svg xmlns="http://www.w3.org/2000/svg"><image/><a xlink:href="alert(1)">x</a><use href="#local-glyph"/><image href="data:image/png;base64,abc"/></svg>'
);

expect(
sanitizeSvgAsset('<svg xmlns="http://www.w3.org/2000/svg"><path d="M3 9h4"/></svg>')
).toBe('<svg xmlns="http://www.w3.org/2000/svg"><path d="M3 9h4"/></svg>');
expect(
sanitizeSvgAsset('<svg data-label="a > b" xmlns="http://www.w3.org/2000/svg"><path/></svg>')
).toBe('<svg data-label="a > b" xmlns="http://www.w3.org/2000/svg"><path/></svg>');
expect(sanitizeSvgAsset('<svg xmlns=""><path/></svg>')).toBe(
'<svg xmlns="http://www.w3.org/2000/svg"><path/></svg>'
);
expect(sanitizeSvgAsset('<svg xmlns="https://example.test/svg"><path/></svg>')).toBe(
'<svg xmlns="http://www.w3.org/2000/svg"><path/></svg>'
);
expect(sanitizeSvgAsset('<svg XMLNS="http://www.w3.org/2000/svg"><path/></svg>')).toBe(
'<svg xmlns="http://www.w3.org/2000/svg" XMLNS="http://www.w3.org/2000/svg"><path/></svg>'
);
expect(
sanitizeSvgAsset('<svg xmlns="http://www.w3.org/2000/svg" xmlns="https://example.test/svg"><path/></svg>')
).toBe('<svg xmlns="http://www.w3.org/2000/svg"><path/></svg>');
expect(
sanitizeSvgAsset('<svg xmlns="" xmlns="https://example.test/svg"><path/></svg>')
).toBe('<svg xmlns="http://www.w3.org/2000/svg"><path/></svg>');
expect(
sanitizeSvgAsset('<?xml version="1.0" encoding="UTF-8"?>\n<!-- icon -->\n<svg viewBox="0 0 1 1"><path/></svg>')
).toBe(
'<?xml version="1.0" encoding="UTF-8"?>\n<!-- icon -->\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><path/></svg>'
);
expect(
sanitizeSvgAsset('<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg"><path/></svg>')
).toBe('<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg"><path/></svg>');
expect(
sanitizeSvgAsset('<?source-generator value="a > b"?>\n<svg viewBox="0 0 1 1"><path/></svg>')
).toBe(
'<?source-generator value="a > b"?>\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><path/></svg>'
);
expect(
sanitizeSvgAsset('<!DOCTYPE svg [<!ENTITY label "a > b">]>\n<svg viewBox="0 0 1 1"><path/></svg>')
).toBe(
'<!DOCTYPE svg [<!ENTITY label "a > b">]>\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><path/></svg>'
);
expect(
sanitizeSvgAsset('<!DOCTYPE svg [<!-- ]> --><?probe value="]>"?><!ENTITY label "a > b">]>\n<svg viewBox="0 0 1 1"><path/></svg>')
).toBe(
'<!DOCTYPE svg [<!-- ]> --><?probe value="]>"?><!ENTITY label "a > b">]>\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><path/></svg>'
);
expect(
sanitizeSvgAsset('<svg xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="#glyph"/></svg>')
).toBe(
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="#glyph"/></svg>'
);
expect(sanitizeSvgAsset('<div data-asset="unchanged">bytes</div>')).toBe(
'<div data-asset="unchanged">bytes</div>'
);
});
});
64 changes: 64 additions & 0 deletions packages/blocks-engine/src/__tests__/theme-svg-asset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { chromium } from 'playwright';
import { describe, expect, it } from 'vitest';

import { emptyNativeRenderOut } from '../theme/native-block-builders.js';
import { iconImageBlock } from '../theme/native-media.js';

describe('captured SVG theme assets', () => {
it('emits namespace-free captured SVG icons that Chromium decodes as external images', async () => {
const out = emptyNativeRenderOut();
const markup = iconImageBlock(
{
kind: 'svg',
markup: '<svg fill="#ababab" viewBox="0 0 24 24" width="24" height="24"><path d="M2 2h20v20H2z"/></svg>',
width: 24,
height: 24,
},
out,
{ iconCounter: 0, mediaTextIndex: 0, paletteTokens: [], fontFamilies: [] }
);

expect(markup).toContain("get_theme_file_uri('assets/icon-0.svg')");
expect(out.iconAssets).toHaveLength(1);

const asset = out.iconAssets[0];
expect(asset.path).toBe('assets/icon-0.svg');
const outputDir = await mkdtemp(join(tmpdir(), 'blocks-engine-svg-asset-'));
const server = createServer((request, response) => {
if (request.url === '/icon.svg') {
response.writeHead(200, { 'content-type': 'image/svg+xml' }).end(asset.svg);
return;
}
response.writeHead(404).end();
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});

try {
const address = server.address();
if (!address || typeof address === 'string') throw new Error('SVG test server did not expose a TCP port.');
await writeFile(
join(outputDir, 'index.html'),
`<img id="icon" src="http://127.0.0.1:${address.port}/icon.svg"><script>const image = document.querySelector('#icon'); image.decode().then(() => document.body.dataset.decode = image.naturalWidth + 'x' + image.naturalHeight).catch(() => document.body.dataset.decode = 'failed');</script>`
);
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.goto(`file://${join(outputDir, 'index.html')}`);
await page.waitForFunction(() => document.body.dataset.decode !== undefined);
expect(await page.locator('body').getAttribute('data-decode')).toBe('24x24');
} finally {
await browser.close();
}
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
await rm(outputDir, { recursive: true, force: true });
}
});
});
Loading
Loading