From 3315605a74345acba2dd553c0c20c5ce2d515d97 Mon Sep 17 00:00:00 2001 From: Mikey Binns Date: Sat, 6 Apr 2024 01:14:02 +0100 Subject: [PATCH 1/4] convert shortcode package to typescript --- packages/shortcode/package.json | 1 + packages/shortcode/src/{index.js => index.ts} | 178 ++++++++++++------ .../shortcode/src/test/{index.js => index.ts} | 80 ++++++-- packages/shortcode/tsconfig.json | 9 + tsconfig.json | 1 + 5 files changed, 202 insertions(+), 67 deletions(-) rename packages/shortcode/src/{index.js => index.ts} (73%) rename packages/shortcode/src/test/{index.js => index.ts} (82%) create mode 100644 packages/shortcode/tsconfig.json diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index a3c3ce78a4edf0..6af33b68c66392 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -24,6 +24,7 @@ "main": "build/index.js", "module": "build-module/index.js", "react-native": "src/index", + "types": "build-types", "dependencies": { "@babel/runtime": "^7.16.0", "memize": "^2.0.1" diff --git a/packages/shortcode/src/index.js b/packages/shortcode/src/index.ts similarity index 73% rename from packages/shortcode/src/index.js rename to packages/shortcode/src/index.ts index 04e69c272378b3..8a43c32140776e 100644 --- a/packages/shortcode/src/index.js +++ b/packages/shortcode/src/index.ts @@ -11,6 +11,16 @@ import memize from 'memize'; * @property {Object} named Object with named attributes. * @property {Array} numeric Array with numeric attributes. */ +type WPShortcodeAttrs = { + /** + * Object with named attributes. + */ + named: Record< string, string >; + /** + * Array with numeric attributes. + */ + numeric: string[]; +}; /** * Shortcode object. @@ -23,6 +33,24 @@ import memize from 'memize'; * @property {string} type Shortcode type: `self-closing`, * `closed`, or `single`. */ +type WPShortcodeOptions = { + /** + * Shortcode tag. + */ + tag: string; + /** + * Shortcode attributes. + */ + attrs: string | WPShortcodeAttrs | WPShortcodeAttrs[ 'named' ]; + /** + * Shortcode content. + */ + content: string; + /** + * Shortcode type. + */ + type: 'single' | 'self-closing' | 'closed'; +}; /** * @typedef {Object} WPShortcodeMatch @@ -31,6 +59,20 @@ import memize from 'memize'; * @property {string} content Matched content. * @property {WPShortcode} shortcode Shortcode instance of the match. */ +type WPShortcodeMatch = { + /** + * Index the shortcode is found at. + */ + index: number; + /** + * Matched content. + */ + content: string; + /** + * Shortcode instance of the match. + */ + shortcode: WPShortcode; +}; /** * Find the next matching shortcode. @@ -41,7 +83,11 @@ import memize from 'memize'; * * @return {WPShortcodeMatch | undefined} Matched information. */ -export function next( tag, text, index = 0 ) { +export function next( + tag: string, + text: string, + index = 0 +): WPShortcodeMatch | undefined { const re = regexp( tag ); re.lastIndex = index; @@ -88,24 +134,28 @@ export function next( tag, text, index = 0 ) { * * @return {string} Text with shortcodes replaced. */ -export function replace( tag, text, callback ) { - return text.replace( - regexp( tag ), - function ( match, left, $3, attrs, slash, content, closing, right ) { - // If both extra brackets exist, the shortcode has been properly - // escaped. - if ( left === '[' && right === ']' ) { - return match; - } +export function replace( + tag: string, + text: string, + callback: ( shortcode: WPShortcode ) => any +): string { + return text.replace( regexp( tag ), function ( ...args ) { + const [ match, left, , , , , , right ] = args; + // If both extra brackets exist, the shortcode has been properly + // escaped. + if ( left === '[' && right === ']' ) { + return match; + } - // Create the match object and pass it through the callback. - const result = callback( fromMatch( arguments ) ); + // Create the match object and pass it through the callback. + const result = callback( + fromMatch( args as unknown as RegExpExecArray ) + ); - // Make sure to return any of the extra brackets if they weren't used to - // escape the shortcode. - return result || result === '' ? left + result + right : match; - } - ); + // Make sure to return any of the extra brackets if they weren't used to + // escape the shortcode. + return result || result === '' ? left + result + right : match; + } ); } /** @@ -121,7 +171,7 @@ export function replace( tag, text, callback ) { * * @return {string} String representation of the shortcode. */ -export function string( options ) { +export function string( options: WPShortcodeOptions ): string { return new shortcode( options ).string(); } @@ -145,7 +195,7 @@ export function string( options ) { * * @return {RegExp} Shortcode RegExp. */ -export function regexp( tag ) { +export function regexp( tag: string ): RegExp { return new RegExp( '\\[(\\[?)(' + tag + @@ -171,9 +221,9 @@ export function regexp( tag ) { * * @return {WPShortcodeAttrs} Parsed shortcode attributes. */ -export const attrs = memize( ( text ) => { - const named = {}; - const numeric = []; +export const attrs = memize( ( text: string ): WPShortcodeAttrs => { + const named: Record< string, string > = {}; + const numeric: string[] = []; // This regular expression is reused from `shortcode_parse_atts()` in // `wp-includes/shortcodes.php`. @@ -195,7 +245,7 @@ export const attrs = memize( ( text ) => { // Map zero-width spaces to actual spaces. text = text.replace( /[\u00a0\u200b]/g, ' ' ); - let match; + let match: RegExpExecArray | null; // Match and normalize attributes. while ( ( match = pattern.exec( text ) ) ) { @@ -228,8 +278,8 @@ export const attrs = memize( ( text ) => { * * @return {WPShortcode} Shortcode instance. */ -export function fromMatch( match ) { - let type; +export function fromMatch( match: RegExpExecArray ): WPShortcode { + let type: 'single' | 'self-closing' | 'closed'; if ( match[ 4 ] ) { type = 'self-closing'; @@ -247,6 +297,19 @@ export function fromMatch( match ) { } ); } +function isWPShortcodeAttrsObject( + attributes: WPShortcodeAttrs | WPShortcodeAttrs[ 'named' ] +): attributes is WPShortcodeAttrs { + const attributeTypes = [ 'named', 'numeric' ]; + return ( + Object.keys( attributes ).length === attributeTypes.length && + attributeTypes.every( + ( attributeType, key ) => + attributeType === Object.keys( attributes )[ key ] + ) + ); +} + /** * Creates a shortcode instance. * @@ -259,10 +322,17 @@ export function fromMatch( match ) { * * @return {WPShortcode} Shortcode instance. */ -const shortcode = Object.assign( - function ( options ) { +class shortcode { + tag?: string; + attrs: WPShortcodeAttrs; + type?: 'single' | 'self-closing' | 'closed'; + content?: string; + + constructor( options: WPShortcodeOptions ) { const { tag, attrs: attributes, type, content } = options || {}; - Object.assign( this, { tag, type, content } ); + this.tag = tag; + this.type = type; + this.content = content; // Ensure we have a correctly formatted `attrs` object. this.attrs = { @@ -274,16 +344,11 @@ const shortcode = Object.assign( return; } - const attributeTypes = [ 'named', 'numeric' ]; - // Parse a string of attributes. if ( typeof attributes === 'string' ) { this.attrs = attrs( attributes ); // Identify a correctly formatted `attrs` object. - } else if ( - attributes.length === attributeTypes.length && - attributeTypes.every( ( t, key ) => t === attributes[ key ] ) - ) { + } else if ( isWPShortcodeAttrsObject( attributes ) ) { this.attrs = attributes; // Handle a flat object of attributes. } else { @@ -291,18 +356,13 @@ const shortcode = Object.assign( this.set( key, value ); } ); } - }, - { - next, - replace, - string, - regexp, - attrs, - fromMatch, } -); -Object.assign( shortcode.prototype, { + next = next; + replace = replace; + regexp = regexp; + fromMatch = fromMatch; + /** * Get a shortcode attribute. * @@ -313,11 +373,12 @@ Object.assign( shortcode.prototype, { * * @return {string} Attribute value. */ - get( attr ) { - return this.attrs[ typeof attr === 'number' ? 'numeric' : 'named' ][ - attr - ]; - }, + get( attr: number | string ): string { + if ( typeof attr === 'number' ) { + return this.attrs.numeric[ attr ]; + } + return this.attrs.named[ attr ]; + } /** * Set a shortcode attribute. @@ -330,18 +391,21 @@ Object.assign( shortcode.prototype, { * * @return {WPShortcode} Shortcode instance. */ - set( attr, value ) { - this.attrs[ typeof attr === 'number' ? 'numeric' : 'named' ][ attr ] = - value; + set( attr: number | string, value: string ): WPShortcode { + if ( typeof attr === 'number' ) { + this.attrs.numeric[ attr ] = value; + } else { + this.attrs.named[ attr ] = value; + } return this; - }, + } /** * Transform the shortcode into a string. * * @return {string} String representation of the shortcode. */ - string() { + string(): string { let text = '[' + this.tag; this.attrs.numeric.forEach( ( value ) => { @@ -373,7 +437,9 @@ Object.assign( shortcode.prototype, { // Add the closing tag. return text + '[/' + this.tag + ']'; - }, -} ); + } +} + +type WPShortcode = shortcode; export default shortcode; diff --git a/packages/shortcode/src/test/index.js b/packages/shortcode/src/test/index.ts similarity index 82% rename from packages/shortcode/src/test/index.js rename to packages/shortcode/src/test/index.ts index e385dacb0870b0..6039878394996d 100644 --- a/packages/shortcode/src/test/index.js +++ b/packages/shortcode/src/test/index.ts @@ -1,13 +1,13 @@ /** * Internal dependencies */ -import { next, replace, attrs } from '../'; +import shortcode, { next, replace, attrs } from '..'; describe( 'shortcode', () => { describe( 'next', () => { it( 'should find the shortcode', () => { const result = next( 'foo', 'this has the [foo] shortcode' ); - expect( result.index ).toBe( 13 ); + expect( result?.index ).toBe( 13 ); } ); it( 'should find the shortcode with attributes', () => { @@ -15,7 +15,7 @@ describe( 'shortcode', () => { 'foo', 'this has the [foo param="foo"] shortcode' ); - expect( result.index ).toBe( 13 ); + expect( result?.index ).toBe( 13 ); } ); it( 'should not find shortcodes that are not there', () => { @@ -33,10 +33,10 @@ describe( 'shortcode', () => { it( 'should find the shortcode when told to start looking beyond the start of the string', () => { const result1 = next( 'foo', 'this has the [foo] shortcode', 12 ); - expect( result1.index ).toBe( 13 ); + expect( result1?.index ).toBe( 13 ); const result2 = next( 'foo', 'this has the [foo] shortcode', 13 ); - expect( result2.index ).toBe( 13 ); + expect( result2?.index ).toBe( 13 ); const result3 = next( 'foo', 'this has the [foo] shortcode', 14 ); expect( result3 ).toBe( undefined ); @@ -48,7 +48,7 @@ describe( 'shortcode', () => { 'this has the [foo] shortcode [foo] twice', 14 ); - expect( result.index ).toBe( 29 ); + expect( result?.index ).toBe( 29 ); } ); it( 'should not find escaped shortcodes', () => { @@ -66,18 +66,18 @@ describe( 'shortcode', () => { it( 'should find shortcodes that are incorrectly escaped by newlines', () => { const result1 = next( 'foo', 'this has the [\n[foo]] shortcode' ); - expect( result1.index ).toBe( 15 ); + expect( result1?.index ).toBe( 15 ); const result2 = next( 'foo', 'this has the [[foo]\n] shortcode' ); - expect( result2.index ).toBe( 14 ); + expect( result2?.index ).toBe( 14 ); } ); it( 'should still work when there are not equal amounts of square brackets', () => { const result1 = next( 'foo', 'this has the [[foo] shortcode' ); - expect( result1.index ).toBe( 14 ); + expect( result1?.index ).toBe( 14 ); const result2 = next( 'foo', 'this has the [foo]] shortcode' ); - expect( result2.index ).toBe( 13 ); + expect( result2?.index ).toBe( 13 ); } ); it( 'should find the second instances of the shortcode when the first one is escaped', () => { @@ -85,7 +85,7 @@ describe( 'shortcode', () => { 'foo', 'this has the [[foo]] shortcode [foo] twice' ); - expect( result.index ).toBe( 31 ); + expect( result?.index ).toBe( 31 ); } ); it( 'should not find shortcodes that are not full matches', () => { @@ -306,4 +306,62 @@ describe( 'shortcode', () => { expect( result ).toEqual( expected ); } ); } ); + + describe( 'WPShortcode', () => { + it( 'should accept a string as the attribute type', () => { + const result = new shortcode( { + tag: '', + content: '', + type: 'single', + attrs: `param="foo" another='bar' andagain=baz`, + } ).attrs; + const expected = { + named: { + param: 'foo', + another: 'bar', + andagain: 'baz', + }, + numeric: [], + }; + expect( result ).toEqual( expected ); + } ); + it( 'should accept a valid object as the attribute type', () => { + const result = new shortcode( { + tag: '', + content: '', + type: 'single', + attrs: attrs( `param="foo" another='bar' andagain=baz` ), + } ).attrs; + const expected = { + named: { + param: 'foo', + another: 'bar', + andagain: 'baz', + }, + numeric: [], + }; + expect( result ).toEqual( expected ); + } ); + it( 'should accept a shallow named object as the attribute type', () => { + const result = new shortcode( { + tag: '', + content: '', + type: 'single', + attrs: { + param: 'foo', + another: 'bar', + andagain: 'baz', + }, + } ).attrs; + const expected = { + named: { + param: 'foo', + another: 'bar', + andagain: 'baz', + }, + numeric: [], + }; + expect( result ).toEqual( expected ); + } ); + } ); } ); diff --git a/packages/shortcode/tsconfig.json b/packages/shortcode/tsconfig.json new file mode 100644 index 00000000000000..24d6470ee85899 --- /dev/null +++ b/packages/shortcode/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types" + }, + "includes": [ "src/**/*" ] +} diff --git a/tsconfig.json b/tsconfig.json index a273bd22806a84..298550c245f933 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -44,6 +44,7 @@ { "path": "packages/redux-routine" }, { "path": "packages/report-flaky-tests" }, { "path": "packages/rich-text" }, + { "path": "packages/shortcode" }, { "path": "packages/style-engine" }, { "path": "packages/sync" }, { "path": "packages/token-list" }, From 677861bcd5f6632feb974c03345a099091b6f2eb Mon Sep 17 00:00:00 2001 From: Mikey Binns Date: Sat, 6 Apr 2024 01:36:32 +0100 Subject: [PATCH 2/4] add ShortcodeType to ease maintaining --- packages/shortcode/src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/shortcode/src/index.ts b/packages/shortcode/src/index.ts index 8a43c32140776e..190bf8598245b4 100644 --- a/packages/shortcode/src/index.ts +++ b/packages/shortcode/src/index.ts @@ -22,6 +22,8 @@ type WPShortcodeAttrs = { numeric: string[]; }; +type ShortcodeType = 'single' | 'self-closing' | 'closed'; + /** * Shortcode object. * @@ -49,7 +51,7 @@ type WPShortcodeOptions = { /** * Shortcode type. */ - type: 'single' | 'self-closing' | 'closed'; + type: ShortcodeType; }; /** @@ -279,7 +281,7 @@ export const attrs = memize( ( text: string ): WPShortcodeAttrs => { * @return {WPShortcode} Shortcode instance. */ export function fromMatch( match: RegExpExecArray ): WPShortcode { - let type: 'single' | 'self-closing' | 'closed'; + let type: ShortcodeType; if ( match[ 4 ] ) { type = 'self-closing'; @@ -325,7 +327,7 @@ function isWPShortcodeAttrsObject( class shortcode { tag?: string; attrs: WPShortcodeAttrs; - type?: 'single' | 'self-closing' | 'closed'; + type?: ShortcodeType; content?: string; constructor( options: WPShortcodeOptions ) { From 5f0318d5301dd87805e934ef4e926294364c1d46 Mon Sep 17 00:00:00 2001 From: Mikey Binns Date: Sat, 6 Apr 2024 01:14:02 +0100 Subject: [PATCH 3/4] convert shortcode package to typescript --- packages/shortcode/package.json | 1 + packages/shortcode/src/{index.js => index.ts} | 178 ++++++++++++------ .../shortcode/src/test/{index.js => index.ts} | 80 ++++++-- packages/shortcode/tsconfig.json | 9 + tsconfig.json | 1 + 5 files changed, 202 insertions(+), 67 deletions(-) rename packages/shortcode/src/{index.js => index.ts} (73%) rename packages/shortcode/src/test/{index.js => index.ts} (82%) create mode 100644 packages/shortcode/tsconfig.json diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index 10e368d06c9955..3d2a5cc6afa096 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -25,6 +25,7 @@ "main": "build/index.js", "module": "build-module/index.js", "react-native": "src/index", + "types": "build-types", "dependencies": { "@babel/runtime": "^7.16.0", "memize": "^2.0.1" diff --git a/packages/shortcode/src/index.js b/packages/shortcode/src/index.ts similarity index 73% rename from packages/shortcode/src/index.js rename to packages/shortcode/src/index.ts index 04e69c272378b3..8a43c32140776e 100644 --- a/packages/shortcode/src/index.js +++ b/packages/shortcode/src/index.ts @@ -11,6 +11,16 @@ import memize from 'memize'; * @property {Object} named Object with named attributes. * @property {Array} numeric Array with numeric attributes. */ +type WPShortcodeAttrs = { + /** + * Object with named attributes. + */ + named: Record< string, string >; + /** + * Array with numeric attributes. + */ + numeric: string[]; +}; /** * Shortcode object. @@ -23,6 +33,24 @@ import memize from 'memize'; * @property {string} type Shortcode type: `self-closing`, * `closed`, or `single`. */ +type WPShortcodeOptions = { + /** + * Shortcode tag. + */ + tag: string; + /** + * Shortcode attributes. + */ + attrs: string | WPShortcodeAttrs | WPShortcodeAttrs[ 'named' ]; + /** + * Shortcode content. + */ + content: string; + /** + * Shortcode type. + */ + type: 'single' | 'self-closing' | 'closed'; +}; /** * @typedef {Object} WPShortcodeMatch @@ -31,6 +59,20 @@ import memize from 'memize'; * @property {string} content Matched content. * @property {WPShortcode} shortcode Shortcode instance of the match. */ +type WPShortcodeMatch = { + /** + * Index the shortcode is found at. + */ + index: number; + /** + * Matched content. + */ + content: string; + /** + * Shortcode instance of the match. + */ + shortcode: WPShortcode; +}; /** * Find the next matching shortcode. @@ -41,7 +83,11 @@ import memize from 'memize'; * * @return {WPShortcodeMatch | undefined} Matched information. */ -export function next( tag, text, index = 0 ) { +export function next( + tag: string, + text: string, + index = 0 +): WPShortcodeMatch | undefined { const re = regexp( tag ); re.lastIndex = index; @@ -88,24 +134,28 @@ export function next( tag, text, index = 0 ) { * * @return {string} Text with shortcodes replaced. */ -export function replace( tag, text, callback ) { - return text.replace( - regexp( tag ), - function ( match, left, $3, attrs, slash, content, closing, right ) { - // If both extra brackets exist, the shortcode has been properly - // escaped. - if ( left === '[' && right === ']' ) { - return match; - } +export function replace( + tag: string, + text: string, + callback: ( shortcode: WPShortcode ) => any +): string { + return text.replace( regexp( tag ), function ( ...args ) { + const [ match, left, , , , , , right ] = args; + // If both extra brackets exist, the shortcode has been properly + // escaped. + if ( left === '[' && right === ']' ) { + return match; + } - // Create the match object and pass it through the callback. - const result = callback( fromMatch( arguments ) ); + // Create the match object and pass it through the callback. + const result = callback( + fromMatch( args as unknown as RegExpExecArray ) + ); - // Make sure to return any of the extra brackets if they weren't used to - // escape the shortcode. - return result || result === '' ? left + result + right : match; - } - ); + // Make sure to return any of the extra brackets if they weren't used to + // escape the shortcode. + return result || result === '' ? left + result + right : match; + } ); } /** @@ -121,7 +171,7 @@ export function replace( tag, text, callback ) { * * @return {string} String representation of the shortcode. */ -export function string( options ) { +export function string( options: WPShortcodeOptions ): string { return new shortcode( options ).string(); } @@ -145,7 +195,7 @@ export function string( options ) { * * @return {RegExp} Shortcode RegExp. */ -export function regexp( tag ) { +export function regexp( tag: string ): RegExp { return new RegExp( '\\[(\\[?)(' + tag + @@ -171,9 +221,9 @@ export function regexp( tag ) { * * @return {WPShortcodeAttrs} Parsed shortcode attributes. */ -export const attrs = memize( ( text ) => { - const named = {}; - const numeric = []; +export const attrs = memize( ( text: string ): WPShortcodeAttrs => { + const named: Record< string, string > = {}; + const numeric: string[] = []; // This regular expression is reused from `shortcode_parse_atts()` in // `wp-includes/shortcodes.php`. @@ -195,7 +245,7 @@ export const attrs = memize( ( text ) => { // Map zero-width spaces to actual spaces. text = text.replace( /[\u00a0\u200b]/g, ' ' ); - let match; + let match: RegExpExecArray | null; // Match and normalize attributes. while ( ( match = pattern.exec( text ) ) ) { @@ -228,8 +278,8 @@ export const attrs = memize( ( text ) => { * * @return {WPShortcode} Shortcode instance. */ -export function fromMatch( match ) { - let type; +export function fromMatch( match: RegExpExecArray ): WPShortcode { + let type: 'single' | 'self-closing' | 'closed'; if ( match[ 4 ] ) { type = 'self-closing'; @@ -247,6 +297,19 @@ export function fromMatch( match ) { } ); } +function isWPShortcodeAttrsObject( + attributes: WPShortcodeAttrs | WPShortcodeAttrs[ 'named' ] +): attributes is WPShortcodeAttrs { + const attributeTypes = [ 'named', 'numeric' ]; + return ( + Object.keys( attributes ).length === attributeTypes.length && + attributeTypes.every( + ( attributeType, key ) => + attributeType === Object.keys( attributes )[ key ] + ) + ); +} + /** * Creates a shortcode instance. * @@ -259,10 +322,17 @@ export function fromMatch( match ) { * * @return {WPShortcode} Shortcode instance. */ -const shortcode = Object.assign( - function ( options ) { +class shortcode { + tag?: string; + attrs: WPShortcodeAttrs; + type?: 'single' | 'self-closing' | 'closed'; + content?: string; + + constructor( options: WPShortcodeOptions ) { const { tag, attrs: attributes, type, content } = options || {}; - Object.assign( this, { tag, type, content } ); + this.tag = tag; + this.type = type; + this.content = content; // Ensure we have a correctly formatted `attrs` object. this.attrs = { @@ -274,16 +344,11 @@ const shortcode = Object.assign( return; } - const attributeTypes = [ 'named', 'numeric' ]; - // Parse a string of attributes. if ( typeof attributes === 'string' ) { this.attrs = attrs( attributes ); // Identify a correctly formatted `attrs` object. - } else if ( - attributes.length === attributeTypes.length && - attributeTypes.every( ( t, key ) => t === attributes[ key ] ) - ) { + } else if ( isWPShortcodeAttrsObject( attributes ) ) { this.attrs = attributes; // Handle a flat object of attributes. } else { @@ -291,18 +356,13 @@ const shortcode = Object.assign( this.set( key, value ); } ); } - }, - { - next, - replace, - string, - regexp, - attrs, - fromMatch, } -); -Object.assign( shortcode.prototype, { + next = next; + replace = replace; + regexp = regexp; + fromMatch = fromMatch; + /** * Get a shortcode attribute. * @@ -313,11 +373,12 @@ Object.assign( shortcode.prototype, { * * @return {string} Attribute value. */ - get( attr ) { - return this.attrs[ typeof attr === 'number' ? 'numeric' : 'named' ][ - attr - ]; - }, + get( attr: number | string ): string { + if ( typeof attr === 'number' ) { + return this.attrs.numeric[ attr ]; + } + return this.attrs.named[ attr ]; + } /** * Set a shortcode attribute. @@ -330,18 +391,21 @@ Object.assign( shortcode.prototype, { * * @return {WPShortcode} Shortcode instance. */ - set( attr, value ) { - this.attrs[ typeof attr === 'number' ? 'numeric' : 'named' ][ attr ] = - value; + set( attr: number | string, value: string ): WPShortcode { + if ( typeof attr === 'number' ) { + this.attrs.numeric[ attr ] = value; + } else { + this.attrs.named[ attr ] = value; + } return this; - }, + } /** * Transform the shortcode into a string. * * @return {string} String representation of the shortcode. */ - string() { + string(): string { let text = '[' + this.tag; this.attrs.numeric.forEach( ( value ) => { @@ -373,7 +437,9 @@ Object.assign( shortcode.prototype, { // Add the closing tag. return text + '[/' + this.tag + ']'; - }, -} ); + } +} + +type WPShortcode = shortcode; export default shortcode; diff --git a/packages/shortcode/src/test/index.js b/packages/shortcode/src/test/index.ts similarity index 82% rename from packages/shortcode/src/test/index.js rename to packages/shortcode/src/test/index.ts index e385dacb0870b0..6039878394996d 100644 --- a/packages/shortcode/src/test/index.js +++ b/packages/shortcode/src/test/index.ts @@ -1,13 +1,13 @@ /** * Internal dependencies */ -import { next, replace, attrs } from '../'; +import shortcode, { next, replace, attrs } from '..'; describe( 'shortcode', () => { describe( 'next', () => { it( 'should find the shortcode', () => { const result = next( 'foo', 'this has the [foo] shortcode' ); - expect( result.index ).toBe( 13 ); + expect( result?.index ).toBe( 13 ); } ); it( 'should find the shortcode with attributes', () => { @@ -15,7 +15,7 @@ describe( 'shortcode', () => { 'foo', 'this has the [foo param="foo"] shortcode' ); - expect( result.index ).toBe( 13 ); + expect( result?.index ).toBe( 13 ); } ); it( 'should not find shortcodes that are not there', () => { @@ -33,10 +33,10 @@ describe( 'shortcode', () => { it( 'should find the shortcode when told to start looking beyond the start of the string', () => { const result1 = next( 'foo', 'this has the [foo] shortcode', 12 ); - expect( result1.index ).toBe( 13 ); + expect( result1?.index ).toBe( 13 ); const result2 = next( 'foo', 'this has the [foo] shortcode', 13 ); - expect( result2.index ).toBe( 13 ); + expect( result2?.index ).toBe( 13 ); const result3 = next( 'foo', 'this has the [foo] shortcode', 14 ); expect( result3 ).toBe( undefined ); @@ -48,7 +48,7 @@ describe( 'shortcode', () => { 'this has the [foo] shortcode [foo] twice', 14 ); - expect( result.index ).toBe( 29 ); + expect( result?.index ).toBe( 29 ); } ); it( 'should not find escaped shortcodes', () => { @@ -66,18 +66,18 @@ describe( 'shortcode', () => { it( 'should find shortcodes that are incorrectly escaped by newlines', () => { const result1 = next( 'foo', 'this has the [\n[foo]] shortcode' ); - expect( result1.index ).toBe( 15 ); + expect( result1?.index ).toBe( 15 ); const result2 = next( 'foo', 'this has the [[foo]\n] shortcode' ); - expect( result2.index ).toBe( 14 ); + expect( result2?.index ).toBe( 14 ); } ); it( 'should still work when there are not equal amounts of square brackets', () => { const result1 = next( 'foo', 'this has the [[foo] shortcode' ); - expect( result1.index ).toBe( 14 ); + expect( result1?.index ).toBe( 14 ); const result2 = next( 'foo', 'this has the [foo]] shortcode' ); - expect( result2.index ).toBe( 13 ); + expect( result2?.index ).toBe( 13 ); } ); it( 'should find the second instances of the shortcode when the first one is escaped', () => { @@ -85,7 +85,7 @@ describe( 'shortcode', () => { 'foo', 'this has the [[foo]] shortcode [foo] twice' ); - expect( result.index ).toBe( 31 ); + expect( result?.index ).toBe( 31 ); } ); it( 'should not find shortcodes that are not full matches', () => { @@ -306,4 +306,62 @@ describe( 'shortcode', () => { expect( result ).toEqual( expected ); } ); } ); + + describe( 'WPShortcode', () => { + it( 'should accept a string as the attribute type', () => { + const result = new shortcode( { + tag: '', + content: '', + type: 'single', + attrs: `param="foo" another='bar' andagain=baz`, + } ).attrs; + const expected = { + named: { + param: 'foo', + another: 'bar', + andagain: 'baz', + }, + numeric: [], + }; + expect( result ).toEqual( expected ); + } ); + it( 'should accept a valid object as the attribute type', () => { + const result = new shortcode( { + tag: '', + content: '', + type: 'single', + attrs: attrs( `param="foo" another='bar' andagain=baz` ), + } ).attrs; + const expected = { + named: { + param: 'foo', + another: 'bar', + andagain: 'baz', + }, + numeric: [], + }; + expect( result ).toEqual( expected ); + } ); + it( 'should accept a shallow named object as the attribute type', () => { + const result = new shortcode( { + tag: '', + content: '', + type: 'single', + attrs: { + param: 'foo', + another: 'bar', + andagain: 'baz', + }, + } ).attrs; + const expected = { + named: { + param: 'foo', + another: 'bar', + andagain: 'baz', + }, + numeric: [], + }; + expect( result ).toEqual( expected ); + } ); + } ); } ); diff --git a/packages/shortcode/tsconfig.json b/packages/shortcode/tsconfig.json new file mode 100644 index 00000000000000..24d6470ee85899 --- /dev/null +++ b/packages/shortcode/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types" + }, + "includes": [ "src/**/*" ] +} diff --git a/tsconfig.json b/tsconfig.json index 3ab54f66019bca..80ffff5d0b5a32 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -48,6 +48,7 @@ { "path": "packages/redux-routine" }, { "path": "packages/report-flaky-tests" }, { "path": "packages/rich-text" }, + { "path": "packages/shortcode" }, { "path": "packages/style-engine" }, { "path": "packages/sync" }, { "path": "packages/token-list" }, From c37491a75a4b87b41f022799a3490023fe2eb147 Mon Sep 17 00:00:00 2001 From: Mikey Binns Date: Sat, 6 Apr 2024 01:36:32 +0100 Subject: [PATCH 4/4] add ShortcodeType to ease maintaining --- packages/shortcode/src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/shortcode/src/index.ts b/packages/shortcode/src/index.ts index 8a43c32140776e..190bf8598245b4 100644 --- a/packages/shortcode/src/index.ts +++ b/packages/shortcode/src/index.ts @@ -22,6 +22,8 @@ type WPShortcodeAttrs = { numeric: string[]; }; +type ShortcodeType = 'single' | 'self-closing' | 'closed'; + /** * Shortcode object. * @@ -49,7 +51,7 @@ type WPShortcodeOptions = { /** * Shortcode type. */ - type: 'single' | 'self-closing' | 'closed'; + type: ShortcodeType; }; /** @@ -279,7 +281,7 @@ export const attrs = memize( ( text: string ): WPShortcodeAttrs => { * @return {WPShortcode} Shortcode instance. */ export function fromMatch( match: RegExpExecArray ): WPShortcode { - let type: 'single' | 'self-closing' | 'closed'; + let type: ShortcodeType; if ( match[ 4 ] ) { type = 'self-closing'; @@ -325,7 +327,7 @@ function isWPShortcodeAttrsObject( class shortcode { tag?: string; attrs: WPShortcodeAttrs; - type?: 'single' | 'self-closing' | 'closed'; + type?: ShortcodeType; content?: string; constructor( options: WPShortcodeOptions ) {