Skip to content

Commit

Permalink
Handle font licenses when editing theme metadata (#649)
Browse files Browse the repository at this point in the history
* handle font licenses when editing theme metadata

* move download file util

* split string

Co-authored-by: Vicente Canales <1157901+vcanales@users.noreply.github.com>

* remove console log

Co-authored-by: Vicente Canales <1157901+vcanales@users.noreply.github.com>

* fix handle error in getting license

---------

Co-authored-by: Vicente Canales <1157901+vcanales@users.noreply.github.com>
  • Loading branch information
matiasbenedetto and vcanales authored May 30, 2024
1 parent 5470e48 commit 10b9dcb
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 3 deletions.
59 changes: 59 additions & 0 deletions src/editor-sidebar/metadata-editor-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
* Internal dependencies
*/
import { postUpdateThemeMetadata, fetchReadmeData } from '../resolvers';
import { getFontsCreditsText } from '../utils/fonts';

const ALLOWED_SCREENSHOT_MEDIA_TYPES = [
'image/png',
Expand All @@ -48,6 +49,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => {
author_uri: '',
tags_custom: '',
recommended_plugins: '',
font_credits: '',
subfolder: '',
} );

Expand All @@ -56,6 +58,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => {
useSelect( async ( select ) => {
const themeData = select( 'core' ).getCurrentTheme();
const readmeData = await fetchReadmeData();

setTheme( {
name: themeData.name.raw,
description: themeData.description.raw,
Expand All @@ -66,6 +69,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => {
tags_custom: themeData.tags.rendered,
screenshot: themeData.screenshot,
recommended_plugins: readmeData.recommended_plugins,
font_credits: readmeData.fonts,
subfolder:
themeData.stylesheet.lastIndexOf( '/' ) > 1
? themeData.stylesheet.substring(
Expand Down Expand Up @@ -99,6 +103,26 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => {
} );
};

const updateFontCredits = async () => {
try {
const credits = await getFontsCreditsText();
setTheme( { ...theme, font_credits: credits } );
} catch ( error ) {
// eslint-disable-next-line no-alert
alert(
sprintf(
/* translators: %1: error code, %2: error message */
__(
'Error getting font licenses. Code: %1$s. Message: %2$s',
'create-block-theme'
),
error.code,
error.message
)
);
}
};

const onChangeTags = ( newTags ) => {
setTheme( { ...theme, tags_custom: newTags.join( ', ' ) } );
};
Expand Down Expand Up @@ -231,6 +255,41 @@ Plugin Description`,
setTheme( { ...theme, recommended_plugins: value } )
}
/>

<TextareaControl
label={ __( 'Font credits', 'create-block-theme' ) }
help={
<>
<Button
variant="secondary"
onClick={ updateFontCredits }
>
{ __(
'Get updated font credits',
'create-block-theme'
) }
</Button>
<br />
{ __(
'Credits and licensing information for fonts used in the theme.',
'create-block-theme'
) }
<br />
<ExternalLink href="https://make.wordpress.org/themes/handbook/review/required/#1-licensing-copyright">
{ __( 'Read more.', 'create-block-theme' ) }
</ExternalLink>
</>
}
placeholder={ `${ __( 'Font Name', 'create-block-theme' ) }
${ __( 'Copyright', 'create-block-theme' ) }
${ __( 'License', 'create-block-theme' ) }
${ __( 'Source', 'create-block-theme' ) }` }
value={ theme.font_credits }
onChange={ ( value ) =>
setTheme( { ...theme, font_credits: value } )
}
/>

<BaseControl>
<BaseControl.VisualLabel>
{ __( 'Screenshot', 'create-block-theme' ) }
Expand Down
2 changes: 1 addition & 1 deletion src/landing-page/landing-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
* Internal dependencies
*/
import { downloadExportedTheme } from '../resolvers';
import { downloadFile } from '../utils';
import downloadFile from '../utils/download-file';
import { CreateThemeModal } from './create-modal';

export default function LandingPage() {
Expand Down
2 changes: 1 addition & 1 deletion src/plugin-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { CreateVariationPanel } from './editor-sidebar/create-variation-panel';
import { ThemeMetadataEditorModal } from './editor-sidebar/metadata-editor-modal';
import ScreenHeader from './editor-sidebar/screen-header';
import { downloadExportedTheme } from './resolvers';
import { downloadFile } from './utils';
import downloadFile from './utils/download-file';

const CreateBlockThemePlugin = () => {
const [ isEditorOpen, setIsEditorOpen ] = useState( false );
Expand Down
11 changes: 11 additions & 0 deletions src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,14 @@ export async function downloadExportedTheme() {
parse: false,
} );
}

export async function getFontFamilies() {
const response = await apiFetch( {
path: '/create-block-theme/v1/font-families',
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
} );
return response.data;
}
8 changes: 7 additions & 1 deletion src/utils.js → src/utils/download-file.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export async function downloadFile( response ) {
/*
* Download a file from in a browser.
*
* @param {Response} response The response object from a fetch request.
* @return {void}
*/
export default async function downloadFile( response ) {
const blob = await response.blob();
const filename = response.headers
.get( 'Content-Disposition' )
Expand Down
141 changes: 141 additions & 0 deletions src/utils/fonts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Internal dependencies
*/
import { getFontFamilies } from '../resolvers';
import { Font } from '../lib/lib-font/lib-font.browser';

/**
* Fetch a file from a URL and return it as an ArrayBuffer.
*
* @param {string} url The URL of the file to fetch.
* @return {Promise<ArrayBuffer>} The file as an ArrayBuffer.
*/
async function fetchFileAsArrayBuffer( url ) {
const response = await fetch( url );
if ( ! response.ok ) {
throw new Error( 'Network response was not ok.' );
}
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer;
}

/**
* Retrieves the licensing information of a font file given its URL.
*
* This function fetches the file as an ArrayBuffer, initializes a font object, and extracts licensing details from the font's OpenType tables.
*
* @param {string} url - The URL pointing directly to the font file. The URL should be a direct link to the file and publicly accessible.
* @return {Promise<Object>} A promise that resolves to an object containing the font's licensing details.
*
* The returned object includes the following properties (if available in the font's OpenType tables):
* - fontName: The full font name.
* - copyright: Copyright notice.
* - source: Unique identifier for the font's source.
* - license: License description.
* - licenseURL: URL to the full license text.
*/
async function getFontFileLicenseFromUrl( url ) {
const buffer = await fetchFileAsArrayBuffer( url );
const fontObj = new Font( 'Uploaded Font' );
fontObj.fromDataBuffer( buffer, url );
// Assuming that fromDataBuffer triggers onload event and returning a Promise
const onloadEvent = await new Promise(
( resolve ) => ( fontObj.onload = resolve )
);
const font = onloadEvent.detail.font;
const { name: nameTable } = font.opentype.tables;
return {
fontName: nameTable.get( 16 ) || nameTable.get( 1 ),
copyright: nameTable.get( 0 ),
source: nameTable.get( 11 ),
license: nameTable.get( 13 ),
licenseURL: nameTable.get( 14 ),
};
}

/**
* Get the license for a font family.
*
* @param {Object} fontFamily The font family in theme.json format.
* @return {Promise<Object|null>} A promise that resolved to the font license object if sucessful or null if the font family does not have a fontFace property.
*/
async function getFamilyLicense( fontFamily ) {
// If the font family does not have a fontFace property, return an empty string.
if ( ! fontFamily.fontFace?.length ) {
return null;
}

// Load the fontFace from the first fontFace object in the font family.
const fontFace = fontFamily.fontFace[ 0 ];
const faceUrl = Array.isArray( fontFace.src )
? fontFace.src[ 0 ]
: fontFace.src;

// Get the license from the font face url.
return await getFontFileLicenseFromUrl( faceUrl );
}

/**
* Get the text for the font licenses of all the fonts defined in the theme.
*
* @return {Promise<Array>} A promise that resolves to an array containing font credits objects.
*/
async function getFontsCreditsArray() {
const fontFamilies = await getFontFamilies();

//Remove duplicates. Removes the font families that have the same fontFamily property.
const uniqueFontFamilies = fontFamilies.filter(
( fontFamily, index, self ) =>
index ===
self.findIndex( ( t ) => t.fontFamily === fontFamily.fontFamily )
);

const credits = [];

// Iterate over fontFamilies and get the license for each family
for ( const fontFamily of uniqueFontFamilies ) {
const fontCredits = await getFamilyLicense( fontFamily );
if ( fontCredits ) {
credits.push( fontCredits );
}
}

return credits;
}

/**
* Get the text for the font licenses of all the fonts defined in the theme.
*
* @return {Promise<string>} A promise that resolves to an string containing the formatted font licenses.
*/
export async function getFontsCreditsText() {
const creditsArray = await getFontsCreditsArray();
const credits = creditsArray
.reduce( ( acc, credit ) => {
// skip if fontName is not available
if ( ! credit.fontName ) {
// continue
return acc;
}

acc.push( credit.fontName );

if ( credit.copyright ) {
acc.push( credit.copyright );
}

if ( credit.source ) {
acc.push( `Source: ${ credit.source }` );
}

if ( credit.license ) {
acc.push( `License: ${ credit.license }` );
}

acc.push( '' );

return acc;
}, [] )
.join( '\n' );
return credits;
}

0 comments on commit 10b9dcb

Please sign in to comment.