Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert shortcode package to TypeScript #60526

Open
wants to merge 2 commits into
base: trunk
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions packages/shortcode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
180 changes: 124 additions & 56 deletions packages/shortcode/src/index.js → packages/shortcode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ 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[];
};

type ShortcodeType = 'single' | 'self-closing' | 'closed';

/**
* Shortcode object.
Expand All @@ -23,6 +35,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: ShortcodeType;
};

/**
* @typedef {Object} WPShortcodeMatch
Expand All @@ -31,6 +61,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.
Expand All @@ -41,7 +85,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;
Expand Down Expand Up @@ -88,24 +136,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;
} );
}

/**
Expand All @@ -121,7 +173,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();
}

Expand All @@ -145,7 +197,7 @@ export function string( options ) {
*
* @return {RegExp} Shortcode RegExp.
*/
export function regexp( tag ) {
export function regexp( tag: string ): RegExp {
return new RegExp(
'\\[(\\[?)(' +
tag +
Expand All @@ -171,9 +223,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`.
Expand All @@ -195,7 +247,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 ) ) ) {
Expand Down Expand Up @@ -228,8 +280,8 @@ export const attrs = memize( ( text ) => {
*
* @return {WPShortcode} Shortcode instance.
*/
export function fromMatch( match ) {
let type;
export function fromMatch( match: RegExpExecArray ): WPShortcode {
let type: ShortcodeType;

if ( match[ 4 ] ) {
type = 'self-closing';
Expand All @@ -247,6 +299,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.
*
Expand All @@ -259,10 +324,17 @@ export function fromMatch( match ) {
*
* @return {WPShortcode} Shortcode instance.
*/
const shortcode = Object.assign(
function ( options ) {
class shortcode {
tag?: string;
attrs: WPShortcodeAttrs;
type?: ShortcodeType;
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 = {
Expand All @@ -274,35 +346,25 @@ 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 {
Object.entries( attributes ).forEach( ( [ key, value ] ) => {
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.
*
Expand All @@ -313,11 +375,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.
Expand All @@ -330,18 +393,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 ) => {
Expand Down Expand Up @@ -373,7 +439,9 @@ Object.assign( shortcode.prototype, {

// Add the closing tag.
return text + '[/' + this.tag + ']';
},
} );
}
}

type WPShortcode = shortcode;

export default shortcode;
Loading
Loading