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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/cli/ai/block-content-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const BLOCK_COMMENT_PATTERN = /<!--\s*\/?wp:[^>]*-->/g;
const SINGLE_SCRIPT_PATTERN = /^<script(?:\s[^>]*)?>[\s\S]*<\/script>\s*$/i;
Comment thread
youknowriad marked this conversation as resolved.
Dismissed
const SINGLE_SVG_PATTERN = /^<svg(?:\s[^>]*)?>[\s\S]*<\/svg>\s*$/i;
const FORM_MARKUP_PATTERN = /<(form|input|select|textarea|label|fieldset|legend|option)\b/i;
const INTERACTION_MARKUP_PATTERN = /<(marquee)\b/i;

export function getHtmlBlockPolicyIssues( content: string ): string[] {
const html = content.replace( BLOCK_COMMENT_PATTERN, '' ).trim();
if ( ! html || isAllowedHtmlBlockContent( html ) ) {
return [];
}

return [
'core/html contains markup that should use editable core blocks. Use core/group, core/columns, core/heading, core/paragraph, core/list, core/image, core/buttons, and theme CSS instead. Keep core/html only for inline SVG, form/input markup, interaction markup with no block equivalent, or a single script block.',
];
}

function isAllowedHtmlBlockContent( html: string ): boolean {
return (
SINGLE_SCRIPT_PATTERN.test( html ) ||
SINGLE_SVG_PATTERN.test( html ) ||
FORM_MARKUP_PATTERN.test( html ) ||
INTERACTION_MARKUP_PATTERN.test( html )
);
}
30 changes: 29 additions & 1 deletion apps/cli/ai/block-validator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getHtmlBlockPolicyIssues } from 'cli/ai/block-content-policy';
import { EditorPage } from 'cli/ai/browser-utils';

interface BlockValidationResult {
Expand Down Expand Up @@ -167,7 +168,7 @@ export async function validateBlocks(
/* eslint-enable @typescript-eslint/no-explicit-any */
}, content );

return report as ValidationReport;
return applyBlockContentPolicy( report as ValidationReport );
} catch ( error ) {
// If navigation or evaluation failed, discard the cached page so the
// next call gets a fresh one.
Expand All @@ -186,6 +187,33 @@ export async function validateBlocks(
}
}

function applyBlockContentPolicy( report: ValidationReport ): ValidationReport {
const results = report.results.map( ( result ) => {
if ( result.blockName !== 'core/html' ) {
return result;
}

const policyIssues = getHtmlBlockPolicyIssues( result.originalContent );
if ( policyIssues.length === 0 ) {
return result;
}

return {
...result,
isValid: false,
issues: [ ...result.issues, ...policyIssues ],
};
} );

const validBlocks = results.filter( ( result ) => result.isValid ).length;
return {
...report,
results,
validBlocks,
invalidBlocks: results.length - validBlocks,
};
}

/** Clean up all cached editor pages. */
export async function cleanupValidatorPages(): Promise< void > {
for ( const ep of editorPages.values() ) {
Expand Down
37 changes: 37 additions & 0 deletions apps/cli/ai/tests/block-content-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest';
import { getHtmlBlockPolicyIssues } from 'cli/ai/block-content-policy';

describe( 'HTML block content policy', () => {
it( 'allows inline SVG markup', () => {
expect(
getHtmlBlockPolicyIssues(
'<!-- wp:html --><svg viewBox="0 0 10 10"><path d="M0 0h10v10H0z" /></svg><!-- /wp:html -->'
)
).toEqual( [] );
} );

it( 'allows a single script block', () => {
expect(
getHtmlBlockPolicyIssues(
'<!-- wp:html --><script>document.body.classList.add("ready");</script><!-- /wp:html -->'
)
).toEqual( [] );
} );

it( 'allows form markup', () => {
expect(
getHtmlBlockPolicyIssues(
'<!-- wp:html --><form><label>Email<input type="email" /></label></form><!-- /wp:html -->'
)
).toEqual( [] );
} );

it( 'flags layout and text markup that should use core blocks', () => {
const issues = getHtmlBlockPolicyIssues(
'<!-- wp:html --><section class="hero"><h1>Auran</h1><p>Precision furniture.</p></section><!-- /wp:html -->'
);

expect( issues ).toHaveLength( 1 );
expect( issues[ 0 ] ).toContain( 'should use editable core blocks' );
} );
} );
2 changes: 1 addition & 1 deletion apps/cli/ai/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ const runWpCliTool = tool(
const validateBlocksTool = tool(
'validate_blocks',
"Validates WordPress block content by running each block through its save() function in the site's block editor (real browser). " +
'The site must be running. Returns per-block validation results with expected HTML for invalid blocks.',
'The site must be running. Also flags core/html blocks used for editable layout or text content. Returns per-block validation results with expected HTML for invalid blocks.',
{
nameOrPath: z
.string()
Expand Down
22 changes: 22 additions & 0 deletions eval/promptfoo.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,25 @@ tests:
}
return { pass: true, score: 1, reason: 'no wp_cli call used --post_content-file=' };
});
- type: javascript
value: |
return import('node:fs').then(({ readFileSync }) => {
const marker = output.split(/\r?\n/).map(l => l.trim()).find(l => l.startsWith('EVAL_RUNNER_RESULT_FILE='));
if (!marker) return { pass: false, score: 0, reason: `no result-file marker on stdout; got: ${output.slice(0, 200)}` };
const d = JSON.parse(readFileSync(marker.slice('EVAL_RUNNER_RESULT_FILE='.length), 'utf8'));
const validationResults = (d.toolResults || []).filter(r => r.toolName === 'validate_blocks');
if (validationResults.length === 0) {
return { pass: false, score: 0, reason: 'validate_blocks was not called after building the page' };
}
const last = validationResults[validationResults.length - 1];
const text = last.text || '';
const failed = last.isError || /Invalid blocks:/i.test(text) || !/Validation:\s+\d+\/\d+\s+blocks valid/i.test(text);
if (failed) {
return {
pass: false,
score: 0,
reason: `final validate_blocks result was invalid or inconclusive: ${(text || JSON.stringify(last)).slice(0, 500)}`,
};
}
return { pass: true, score: 1, reason: `validate_blocks passed: ${(last.text || '').split(/\r?\n/)[0]}` };
});
Loading