Skip to content
Closed
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 docs/reference-guides/core-blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ Display code snippets that respect your spacing and tabs.
- **Name:** [core/code](https://developer.wordpress.org/block-editor/reference-guides/core-blocks/core-blocks-text/core-block-code/)
- **Category:** [text](https://developer.wordpress.org/block-editor/reference-guides/core-blocks/core-blocks-text/)
- **Supports:** align (wide), anchor, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight)
- **Attributes:** content
- **Attributes:** content, language

## Column

Expand Down
1 change: 1 addition & 0 deletions packages/block-library/src/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ _Defined via the [`attributes`](https://developer.wordpress.org/block-editor/ref
| Attribute | [Type](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#type-validation) | [Default](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#default-value) | Description |
|-----------|------|---------|-------------|
| `content` | `rich-text` | — | [Source](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#value-source): `rich-text`. [Selector](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#value-source): `code`. [Role](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#role): `content` |
| `language` | `string` | `""` | — |

## Supports

Expand Down
4 changes: 4 additions & 0 deletions packages/block-library/src/code/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"selector": "code",
"__unstablePreserveWhiteSpace": true,
"role": "content"
},
"language": {
"type": "string",
"default": ""
}
},
"supports": {
Expand Down
93 changes: 74 additions & 19 deletions packages/block-library/src/code/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { RichText, useBlockProps } from '@wordpress/block-editor';
import {
RichText,
useBlockProps,
InspectorControls,
} from '@wordpress/block-editor';
import { createBlock, getDefaultBlockName } from '@wordpress/blocks';
import { TextControl, PanelBody } from '@wordpress/components';

/**
* Internal dependencies
*/
import { sanitizeLanguage, toHTMLStr, parseFencedCode } from './utils';

export default function CodeEdit( {
attributes,
Expand All @@ -12,25 +22,70 @@ export default function CodeEdit( {
insertBlocksAfter,
mergeBlocks,
} ) {
const { content, language } = attributes;
const blockProps = useBlockProps();
return (
<pre { ...blockProps }>
<RichText
tagName="code"
identifier="content"
value={ attributes.content }
onChange={ ( content ) => setAttributes( { content } ) }
onRemove={ onRemove }
onMerge={ mergeBlocks }
placeholder={ __( 'Write code…' ) }
aria-label={ __( 'Code' ) }
preserveWhiteSpace
__unstablePastePlainText
__unstableOnSplitAtDoubleLineEnd={ () =>
insertBlocksAfter( createBlock( getDefaultBlockName() ) )
}
style={ { whiteSpace: 'break-spaces' } }
/>
</pre>
<>
<InspectorControls>
<PanelBody title={ __( 'Code Language' ) }>
<TextControl
label={ __( 'Language' ) }
value={ language || '' }
onChange={ ( value ) => {
const nextLanguage = sanitizeLanguage( value );
if ( nextLanguage === ( language || '' ) ) {
return;
}
setAttributes( {
language: nextLanguage,
} );
} }
help={ __(
'Enter a language slug, e.g., php, javascript, python.'
) }
autoComplete="off"
/>
</PanelBody>
</InspectorControls>
<pre { ...blockProps }>
<RichText
tagName="code"
identifier="content"
value={ content }
onChange={ ( newContent ) => {
const parsedFencedCode = parseFencedCode( newContent );

if ( parsedFencedCode ) {
// Compare against the string form of current content
// since parsedContent is always a plain string.
if (
parsedFencedCode.content ===
toHTMLStr( content ) &&
parsedFencedCode.language === ( language || '' )
) {
return;
}

setAttributes( parsedFencedCode );
return;
}

setAttributes( { content: newContent } );
} }
onRemove={ onRemove }
onMerge={ mergeBlocks }
placeholder={ __( 'Write code…' ) }
aria-label={ __( 'Code' ) }
preserveWhiteSpace
__unstablePastePlainText
__unstableOnSplitAtDoubleLineEnd={ () =>
insertBlocksAfter(
createBlock( getDefaultBlockName() )
)
}
style={ { whiteSpace: 'break-spaces' } }
/>
</pre>
</>
);
}
10 changes: 7 additions & 3 deletions packages/block-library/src/code/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ import { RichText, useBlockProps } from '@wordpress/block-editor';
import { escape } from './utils';

export default function save( { attributes } ) {
const { content, language } = attributes;
const codeClassName = language ? `language-${ language }` : undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I just saw this example is in the HTML spec... nice!


return (
<pre { ...useBlockProps.save() }>
<RichText.Content
tagName="code"
className={ codeClassName }
// To do: `escape` encodes characters in shortcodes and URLs to
// prevent embedding in PHP. Ideally checks for the code block,
// or pre/code tags, should be made on the PHP side?
value={ escape(
typeof attributes.content === 'string'
? attributes.content
: attributes.content.toHTMLString( {
typeof content === 'string'
? content
: content.toHTMLString( {
preserveWhiteSpace: true,
} )
) }
Expand Down
160 changes: 159 additions & 1 deletion packages/block-library/src/code/test/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { escape } from '../utils';
import { escape, sanitizeLanguage, toHTMLStr, parseFencedCode } from '../utils';

describe( 'core/code', () => {
describe( 'escape()', () => {
Expand All @@ -20,4 +20,162 @@ describe( 'core/code', () => {
expect( text ).toBe( 'Text https://example.com/test/' );
} );
} );

describe( 'sanitizeLanguage()', () => {
it( 'should strip leading and trailing whitespace', () => {
expect( sanitizeLanguage( ' php ' ) ).toBe( 'php' );
} );

it( 'should remove internal whitespace', () => {
expect( sanitizeLanguage( 'type script' ) ).toBe( 'typescript' );
} );

it( 'should strip a leading language- prefix', () => {
expect( sanitizeLanguage( 'language-php' ) ).toBe( 'php' );
} );

it( 'should strip a leading language- prefix case-insensitively', () => {
expect( sanitizeLanguage( 'LANGUAGE-JavaScript' ) ).toBe(
'JavaScript'
);
} );

it( 'should strip whitespace and language- prefix together', () => {
expect( sanitizeLanguage( ' language-css ' ) ).toBe( 'css' );
} );

it( 'should return an empty string for an empty input', () => {
expect( sanitizeLanguage( '' ) ).toBe( '' );
} );
} );

describe( 'toHTMLStr()', () => {
it( 'should return a plain string unchanged', () => {
expect( toHTMLStr( 'hello world' ) ).toBe( 'hello world' );
} );

it( 'should return an empty string for null', () => {
expect( toHTMLStr( null ) ).toBe( '' );
} );

it( 'should return an empty string for undefined', () => {
expect( toHTMLStr( undefined ) ).toBe( '' );
} );

it( 'should call toHTMLString with preserveWhiteSpace on a RichTextData-like object', () => {
const mockRichText = {
toHTMLString: jest
.fn()
.mockReturnValue( '<strong>bold</strong>' ),
};
const result = toHTMLStr( mockRichText );
expect( mockRichText.toHTMLString ).toHaveBeenCalledWith( {
preserveWhiteSpace: true,
} );
expect( result ).toBe( '<strong>bold</strong>' );
} );
} );

describe( 'parseFencedCode()', () => {
it( 'should return null for non-fenced input', () => {
expect( parseFencedCode( 'just a plain paragraph' ) ).toBeNull();
} );

it( 'should return null when the opening fence has no language', () => {
expect( parseFencedCode( '```\nsome code\n```' ) ).toBeNull();
} );

it( 'should parse a basic fenced code block', () => {
const parsed = parseFencedCode( '```js\nconsole.log( 1 );\n```' );

expect( parsed ).toEqual( {
content: 'console.log( 1 );',
language: 'js',
} );
} );

it( 'should parse an opening fence with spaces before the language', () => {
const parsed = parseFencedCode( '``` php\n<?php echo 1;\n```' );

expect( parsed ).toEqual( {
content: '<?php echo 1;',
language: 'php',
} );
} );

it( 'should parse an opening fence with tabs before the language', () => {
const parsed = parseFencedCode(
'```\t\tjs\nconsole.log( 1 );\n```'
);

expect( parsed ).toEqual( {
content: 'console.log( 1 );',
language: 'js',
} );
} );

it( 'should strip a language- prefix from the info string', () => {
const parsed = parseFencedCode(
'```language-php\n<?php echo 1;\n```'
);

expect( parsed ).toEqual( {
content: '<?php echo 1;',
language: 'php',
} );
} );

it( 'should strip a closing fence without a trailing newline', () => {
const parsed = parseFencedCode( '```js\nconsole.log( 1 );\n```' );

expect( parsed ).toEqual( {
content: 'console.log( 1 );',
language: 'js',
} );
} );

it( 'should strip a closing fence followed by a final newline', () => {
const parsed = parseFencedCode( '```js\nconsole.log( 1 );\n```\n' );

expect( parsed ).toEqual( {
content: 'console.log( 1 );',
language: 'js',
} );
} );

it( 'should preserve content when no closing fence is present', () => {
const parsed = parseFencedCode( '```js\nconsole.log( 1 );' );

expect( parsed ).toEqual( {
content: 'console.log( 1 );',
language: 'js',
} );
} );

it( 'should handle CRLF line endings', () => {
const parsed = parseFencedCode(
'```js\r\nconsole.log( 1 );\r\n```'
);

expect( parsed ).toEqual( {
content: 'console.log( 1 );',
language: 'js',
} );
} );

it( 'should accept end-of-string as the fence terminator when allowEndOfString is true', () => {
const parsed = parseFencedCode( '```js', {
allowEndOfString: true,
} );

expect( parsed ).toEqual( {
content: '',
language: 'js',
} );
} );

it( 'should return null without a trailing newline when allowEndOfString is false', () => {
expect( parseFencedCode( '```js' ) ).toBeNull();
} );
} );
} );
Loading
Loading