-
Notifications
You must be signed in to change notification settings - Fork 3
Your First Extension
This template is designed to help you create Platform.Bible extensions.
In this tutorial, you will create a simple "Hello World" extension to learn the basics of how extensions work.
The paranext-extension-template includes only the essential components needed to run an extension. This section describes these minimal parts. For a full breakdown of an extension’s structure, see Extension Anatomy.
The main directories are:
/src contains the main entry file and a directory for types that contains the .d.ts file for your extension.
Inside main.ts the PAPI backend service is imported, and from it, the logger service to output logs to the console. Each extension must contain a main entry file with activate() and deactivate() functions. In this template, these functions simply log a message to the console.
The types file declares a module that can be populated with type declarations describing the APIs your extension provides to PAPI.
The /webpack directory contains the Webpack configuration files. The only modifications required in this directory are in webpack.config.main.ts, which is described in the next section.
Before you begin to follow the steps to create a "Hello World" extension, if you have not already done so, do the following:
-
Follow the
To installinstructions to set up an environment where you can build and run this Hello World extension inside Platform.Bible. -
Navigate to the paranext-extension-template repo.
-
Select the “Use this template” option. It will ask you to create a new repo for your extension.
-
Clone your extension repo into the same parent directory as paranext-core. (This means you won’t have to reconfigure paths to paranext-core.)
-
If you want to keep your "Hello World" extension updated as Platform.Bible evolves, follow the one-time setup instructions—including fetching from the template and performing an initial merge—in To update this extension from the template to enable your extension to receive updates from the template. If this is just a throwaway exercise, you can skip this step—but since it’s quick and easy, it’s best to set it up if you’re unsure.
In this section, you’ll turn the template into a “Hello World” extension.
The first step in adapting the template is to replace instances of paranext-extension-template with your extension’s details. Below is a list of all the files that need to be edited.
Most of these changes can be done using search and replace, except for the extension manifest files—which must be edited manually—and the types file, which must be renamed.
README.mdpackage-lock.jsonpackage.jsonmanifest.json-
src/types/paranext-extension-template.d.ts(the file name and contents) -
src/main.ts(see details in the WebView provider section below)
-
Search for: paranext-extension-template
Replace with: hello-world
Exclude:
tailwind.cssand theREADME.mdfile (as well as all the things excluded with theUse Exclude Settings and Ignore Filesoption, which is probably selected by default)
- Rename
src/types/paranext-extension-template.d.tstohello-world.d.tsAlso change its name in the line in the
Summarysection of theREADME.mdto avoid confusion later. - Edit the extension manifest files,
package.jsonandmanifest.json, to set the name, publisher, author, etc.The name in
manifest.jsonmust behelloWorld(lowerCamelCase).
Finally, save your changes and run npm start to ensure that Platform.Bible still opens as expected and does not encounter errors when loading the new extension. (Since the extension doesn’t really do much yet, just search in terminal for the message: Extension template is activating! to confirm that it loaded properly.)
The next step is to add a WebView to see the extension inside of Platform.Bible. In order to add a WebView, you’ll need:
- Content to render (React or HTML)
- Any styles needed to display the content properly
- A WebView provider (to define and return the WebView)
- Code inside the
activate()function to register the WebView and optionally open it immediately
Platform.Bible supports both React and HTML WebViews. React WebViews are the most powerful and will likely account for the majority of WebViews in extensions. However, for straightforward scenarios with limited JavaScript and formatting needs—particularly when reusing existing HTML content—HTML WebViews can be sufficient. This tutorial covers both, but feel free to focus on only one or the other if you already anticipate implementing just one type.
URL-based WebViews (using
papi-extension:orhttps:URLs) are also supported but are not covered here.
First create a stylesheet that your React WebViews can use: src/hello-world.scss. For the purpose of this Hello World example, just add a single line to use the Tailwind classes provided by Platform.Bible (styled according to the host application’s theme):
@use 'tailwind';NOTE: For details on adding custom styles in your own extensions, see Styling WebViews.
To create the React WebView, create a new file src/hello-world.web-view.tsx. In it, assign globalThis.webViewComponent to your function component that returns the JSX that you want to show up in your extension. The code below uses the <Button> component from the platform-bible-react library, which helps the extension match the look and feel of the host application.
// src/hello-world.web-view.tsx
import { Button } from "platform-bible-react";
globalThis.webViewComponent = function HelloWorld() {
return (
<div className="tw-flex tw-flex-col tw-gap-4 tw-p-6">
<div className="pr-twp tw-text-2xl tw-font-semibold tw-tracking-tight">
Hello World <span className="tw-text-muted-foreground tw-font-normal">React</span>
</div>
<Button className="pr-twp tw-w-fit tw-min-w-[8rem] tw-gap-2 tw-whitespace-nowrap">Our first button</Button>
</div>
);
};You can also create an HTML WebView. Create the new HTML file src/hello-world-html.web-view.html with:
<html>
<head>
<meta charset="UTF-8">
<title>Hello World HTML WebView</title>
<style>
/* The extension template does not yet support bundling Tailwind into HTML WebViews, so it is
not available. This is a simple approximation for illustrative purposes. */
body {
padding: 1rem;
font-family: sans-serif;
}
.title {
font-size: 1.25rem;
margin-bottom: 1rem;
}
button {
background-color: hsl(222.2 47.4% 11.2%);
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
button:hover {
background-color: hsl(210, 25%, 15%);
}
.output {
margin-top: 1rem;
font-weight: 600;
}
</style>
</head>
<body>
<div class="title">Hello World HTML WebView</div>
<button id="hello-button">
Click Me
</button>
<div id="output" class="output"></div>
<script>
document.getElementById("hello-button").addEventListener("click", () => {
document.getElementById("output").textContent = "Hello from the HTML WebView!";
});
</script>
</body>
</html>In this section you’ll create both a React WebView provider and an HTML WebView provider, corresponding to the two WebViews you just added, so Platform.Bible knows how to render and present the WebViews when they are requested.
Each WebView provider checks that the saved WebView definition matches the type of WebView it is responsible for. If the types do not match, it throws an error. If they do match, it returns a new definition that combines the saved information with the WebView’s title, component, and styles. Platform.Bible then uses this definition to render the WebView in the application.
In src/main.ts import the required types, component, and styles, then define the provider object.
// src/main.ts
import type {
IWebViewProvider,
SavedWebViewDefinition,
WebViewDefinition,
} from '@papi/core';
import helloWorld from "./hello-world.web-view?inline";
import helloWorldReactStyles from './hello-world.scss?inline';
// Although not (currently) a requirement, beginning with the lowerCamelCase version of the
// extension, followed by a period, is a good idea to ensure uniqueness
const reactWebViewType = "helloWorld.react";
/**
* Simple WebView provider that provides React WebViews when PAPI requests them
*/
const reactWebViewProvider: IWebViewProvider = {
async getWebView(
savedWebView: SavedWebViewDefinition
): Promise<WebViewDefinition | undefined> {
if (savedWebView.webViewType !== reactWebViewType)
throw new Error(
`${reactWebViewType} provider received request to provide a ${savedWebView.webViewType} WebView`
);
return {
...savedWebView,
title: "Hello World React",
content: helloWorld,
styles: helloWorldReactStyles,
};
},
};Like the React WebView and stylesheet, the content for an HTML WebView is imported as inline. An HTML WebView provider is very similar to a React WebView. The notable differences are:
- The
webViewTypeconstant on theWebViewDefinitionneeds to be set to a unique value. - The returned object needs to have
contentTypeset to'html'. - HTML WebViews do not specify
styles.
// src/main.ts
import helloWorldHtml from './hello-world-html.web-view.html?inline';
const htmlWebViewType = "helloWorld.html";
/**
* Simple WebView provider that provides HTML WebViews when PAPI requests them
*/
const htmlWebViewProvider: IWebViewProvider = {
async getWebView(
savedWebView: SavedWebViewDefinition
): Promise<WebViewDefinition | undefined> {
if (savedWebView.webViewType !== htmlWebViewType)
throw new Error(
`${reactWebViewType} provider received request to provide a ${savedWebView.webViewType} web view`
);
return {
...savedWebView,
title: "Hello World HTML",
contentType: 'html',
content: helloWorldHtml,
};
},
};To integrate with the PAPI backend service, add two new imports:
- The PAPI backend service itself
- The
ExecutionActivationContextobject that is passed to theactivate()function.The
activate()function should already be defined in thesrc/main.tsfile, but it will require some modifications.
Inside activate(), use the PAPI WebView provider service to register the provider you’ve just created, specifying the type of WebView. Then, use the WebView service to either create a new WebView or get an existing one. Finally, append the awaited promises to the list of registrations at the end, ensuring that they don’t block the execution of other processes.
// src/main.ts
import papi, { logger } from "@papi/backend";
import type {
ExecutionActivationContext,
IWebViewProvider,
SavedWebViewDefinition,
WebViewDefinition,
} from '@papi/core';
export async function activate(context: ExecutionActivationContext): Promise<void> {
logger.info("Hello World is activating!");
const reactWebViewProviderPromise = papi.webViewProviders.registerWebViewProvider(
reactWebViewType,
reactWebViewProvider,
);
const htmlWebViewProviderPromise = papi.webViewProviders.registerWebViewProvider(
htmlWebViewType,
htmlWebViewProvider,
);
// Create WebViews or open an existing webview if one already exists for this type
// Note: here, the use of `existingId: '?'` tells the PAPI not to create a new webview
// if one already exists. The webview that already exists could have been created by anyone
// anywhere; it just has to match `webViewType`. See `hello-someone.ts` for an example of keeping
// an existing webview that was specifically created by `hello-someone`.
// In real extensions, WebViews are often opened in response to user actions, such as selecting a
// menu item, rather than automatically during activation. This tutorial opens the WebView
// directly to simplify testing and demonstration.
papi.webViews.openWebView(reactWebViewType, undefined, { existingId: "?" });
papi.webViews.openWebView(htmlWebViewType, undefined, { existingId: "?" });
context.registrations.add(await reactWebViewProviderPromise, await htmlWebViewProviderPromise);
logger.info("Hello World is finished activating!");
}In a terminal window, run npm start to run Platform.Bible with your extension. When it comes up you should see two new tabs:
-
Hello World React should show a WebView with the message
Hello World Reactand a<button>with the textOur first button. -
Hello World HTML should show a WebView with the message
Hello World HTML WebViewand a<button>with the textClick Me. (This button is already active, so clicking it should display the lineHello from the HTML WebView!.)
Next you’ll make the button in the React WebView do something using commands. You’ll create a shared counter, managed by the extension, that tracks the total number of times the user clicks the button in any instance of the WebView. Then you’ll hook up the button to run a command that increments this shared counter and triggers an event so every WebView is notified when the count changes.
This pattern is useful whenever an extension needs to keep state consistent across multiple WebViews, potentially including WebViews created by other extensions.
To support communication between your extension and Platform.Bible (and potentially other extensions), you’ll define the relevant types in the src\types\hello-world.d.ts file. This includes:
- an event (
DoStuffEvent) that tracks how many times a certain command has been run. - A command (
helloWorld.doStuff) that tells Platform what parameters the command expects and what it returns.
First, declare the event inside the existing hello-world module. The event should expose a single count field representing the number of times the command has been triggered.
Finally, add a papi-shared-types module, declaring a CommandHandlers interface to define the command’s function signature—so PAPI and other extensions can interact with it. This command will accept a string (the input message) and return an object containing a string response and a number occurrence (i.e., the current count of how many times the button has been clicked).
// src\types\hello-world.d.ts
declare module "hello-world" {
/** Network event that informs subscribers when the command `helloWorld.doStuff` is run */
export type DoStuffEvent = {
/** How many times the extension has run the command `helloWorld.doStuff` */
count: number;
};
}
declare module "papi-shared-types" {
export interface CommandHandlers {
"helloWorld.doStuff": (message: string) => {
response: string;
occurrence: number;
};
}
}In the activate() (defined in main.ts), you’ll set up everything needed for the new helloWorld.doStuff command:
- Create a network event emitter (using the network service and the
DoStuffEventtype) to broadcast updates to subscribers. - Register the command with the command service so it can be called from other parts of the app or other extensions.
- Add resources to
context.registrationsso they will be automatically cleaned up when the extension is deactivated.
// src\main.ts
import type { DoStuffEvent } from 'hello-world';
...
export async function activate(context: ExecutionActivationContext) {
...
// Emitter to tell subscribers how many times we have done stuff
const onDoStuffEmitter = papi.network.createNetworkEventEmitter<DoStuffEvent>(
'helloWorld.doStuff',
);
let doStuffCount = 0;
const doStuffCommandPromise = papi.commands.registerCommand(
'helloWorld.doStuff',
(message: string) => {
doStuffCount += 1;
// Inform subscribers of the update
onDoStuffEmitter.emit({ count: doStuffCount });
// Respond to the sender of the command with the news
return {
response: `The template did stuff ${doStuffCount} times! ${message}`,
occurrence: doStuffCount,
};
},
);
context.registrations.add(
await reactWebViewProviderPromise,
await htmlWebViewProviderPromise,
onDoStuffEmitter,
await doStuffCommandPromise,
);
...
}In the React WebView file, you’ll connect the button click to the backend command and keep the UI in sync with the latest count.
To accomplish this, do the following:
-
Add imports
- PAPI frontend service for calling commands and subscribing to events
- The
DoStuffEventtype - React and PAPI hooks for state and event handling
- Create state to track the number of button clicks.
- **Send the command using
sendCommand()when button is clicked. - Log the response (and the time taken) using the logger service.
-
Listen for updates with the
useEventhook so the click counter updates whenever the backend emits the event.
// src\hello-world.web-view.tsx
import papi, { logger } from "@papi/frontend";
import { Button, useEvent } from 'platform-bible-react';
import type { DoStuffEvent } from 'hello-world';
import { useCallback, useState } from 'react';
globalThis.webViewComponent = function HelloWorld() {
const [clicks, setClicks] = useState(0);
useEvent<DoStuffEvent>(
papi.network.getNetworkEvent('helloWorld.doStuff'),
useCallback(({ count }) => setClicks(count), [])
);
return (
<div className="tw-flex tw-flex-col tw-gap-4 tw-p-6">
<div className="pr-twp tw-text-2xl tw-font-semibold tw-tracking-tight">
Hello World <span className="tw-text-muted-foreground tw-font-normal">React</span>
</div>
<Button
className="pr-twp tw-w-fit tw-min-w-[8rem] tw-gap-2 tw-whitespace-nowrap"
onClick={async () => {
const start = performance.now();
const result = await papi.commands.sendCommand(
'helloWorld.doStuff',
'Hello World React Component',
);
logger.info(
`command:helloWorld.doStuff '${result.response}' took ${performance.now() - start} ms`,
);
}}
>
Hi {clicks}
</Button>
</div>
);
};Use npm start to start Platform.Bible with the your updated extension. You should now see a button that says 'Hi' and displays the number of clicks that is calculated using the command and event you created.
Finally, you’ll add and use a data provider class in your extension. Implementing a DataProvider lets your extension (and others) call useData to read and write the data your extension manages.
Inside the types file (src\types\hello-world.d.ts), declare the types of data the provider will use and share. Your Hello World data provider will have three data types:
- Verse – a portion of Scripture at a given reference
Attempting to set the text through this type will fail unless
isEdited: trueis explicitly provided. - VerseTextOverride – the (potentially overridden) text of a verse
Retrieving text through this type always gives the same value as
Verse. Setting text through this type automatically marks the verse as edited, clearly indicating to the user that the content no longer matches the originally published Scripture. - Chapter – a whole chapter of Scripture identified by book name and chapter number
This data is read-only.
Note: The snippet below shows only the new types and interface being added, with the surrounding code included solely to indicate where they belong.
// src\types\hello-world.d.ts
declare module "hello-world" {
import type { DataProviderDataType, IDataProvider } from '@papi/core';
export type ExtensionVerseSetData =
| string
| { text: string; isEdited: boolean };
export type ExtensionVerseDataTypes = {
Verse: DataProviderDataType<
string,
string | undefined,
ExtensionVerseSetData
>;
VerseTextOverride: DataProviderDataType<string, string | undefined, string>;
Chapter: DataProviderDataType<
[book: string, chapter: number],
string | undefined,
never
>;
};
export type ExtensionVerseDataProvider =
IDataProvider<ExtensionVerseDataTypes>;
}
declare module "papi-shared-types" {
import type { ExtensionVerseDataProvider } from "hello-world";
export interface DataProviders {
"helloWorld.quickVerse": ExtensionVerseDataProvider;
}
}In main.ts, but outside of the activate() function, create your data provider class or object. The example below shows a data provider engine class that offers easy access to Scripture data from another data provider. (There are pros and cons to using a class versus an object for a data provider engine.)
First, add or update the imports as shown. Then create the data provider engine, declaring any needed variables, and add a constructor.
// src\main.ts
import { VerseRef } from '@sillsdev/scripture';
import papi, { DataProviderEngine, logger } from '@papi/backend'
import type { DataProviderUpdateInstructions, IDataProviderEngine } from '@papi/core';
import type {
DoStuffEvent,
ExtensionVerseDataTypes,
ExtensionVerseSetData,
} from 'hello-world';
class QuickVerseDataProviderEngine
extends DataProviderEngine<ExtensionVerseDataTypes>
implements IDataProviderEngine<ExtensionVerseDataTypes>
{
verses: { [scrRef: string]: { text: string; isChanged?: boolean } } = {};
/** Latest updated verse reference */
latestVerseRef = 'JHN 11:35';
usfmDataProviderPromise = papi.projectDataProviders.get(
'platformScripture.USFM_Verse',
'32664dc3288a28df2e2bb75ded887fc8f17a15fb',
);
/** Number of times any verse has been modified by a user this session */
editCount = 0;
/** @param editWarning string to prefix edited data */
constructor(public editWarning: string) {
// `DataProviderEngine`'s constructor currently does nothing, but TypeScript requires this call.
super();
this.editWarning = this.editWarning ?? 'editCount =';
}
...
}At this point, you might notice two things in your editor:
- An error stating that
IDataProviderEngine<ExtensionVerseDataTypes>is not implemented. - A warning that the new import
ExtensionVerseSetDatais not yet used.
That’s expected—you haven’t added the get and set functions yet. You’ll implement those methods in a moment, but first you’ll add two helper methods, as shown in the following code snippet:
-
setInternal()– An internal version of the set method that does not send updates. It will be used bysetVerse()andsetEdited()to avoid broadcasting unnecessary changes.
Note: A method whose name starts with
setis normally treated by PAPI as a data type method, meaning a matchinggetInternalmethod would be expected. Because that isn’t the case here, it should be decorated with@papi.dataProviders.decorators.ignoreso PAPI skips it. Alternatively, you could simply give it a name that doesn’t start with set, such as_setInternalorinternalSet.
-
#getSelector()– A private method (note the#) that cannot be called over the network.
// src\main.ts
@papi.dataProviders.decorators.ignore
async setInternal(
selector: string,
data: ExtensionVerseSetData,
): Promise<DataProviderUpdateInstructions<ExtensionVerseDataTypes>> {
// Just get notifications of updates with the 'notify' selector. Nothing to change
if (selector === 'notify') return false;
// You can't change scripture from just a string. You have to pass an object to make intentions explicit.
if (typeof data === 'string' || data instanceof String || !data.isEdited) return false;
// If there is no actual change in the verse text, don't update
if (data.text === this.verses[this.#getSelector(selector)].text) return false;
// Update the verse text, track the latest change, and send an update
this.verses[this.#getSelector(selector)] = {
text: data.text,
isChanged: true,
};
if (selector !== 'latest') this.latestVerseRef = this.#getSelector(selector);
this.editCount += 1;
// Update all data types -- Verse and VerseTextOverride in this case
return '*';
}
#getSelector(selector: string) {
const selectorL = selector.toLowerCase().trim();
return selectorL === 'latest' ? this.latestVerseRef : selectorL;
}For each data type you’ve declared, the data provider engine needs a get<data_type> and a set<data_type>.
// src\main.ts
async setVerse(verseRef: string, data: ExtensionVerseSetData) {
return this.setInternal(verseRef, data);
}
async getVerse(verseRef: string) {
// Just get notifications of updates with the 'notify' selector
if (verseRef === 'notify') return undefined;
const selector = this.#getSelector(verseRef);
// Look up the cached data first.
let responseVerse = this.verses[selector];
// If the verse is not already cached, cache it.
if (!responseVerse) {
// Fetch the verse, cache it, and return it.
try {
const usfmDataProvider = await this.usfmDataProviderPromise;
if (!usfmDataProvider) throw Error('Unable to get USFM data provider');
const verseData = usfmDataProvider.getVerseUSFM(new VerseRef(selector));
responseVerse = { text: (await verseData) ?? `${selector} not found` };
// Cache the verse text, track the latest cached verse, and send an update
this.verses[selector] = responseVerse;
this.latestVerseRef = selector;
this.notifyUpdate();
} catch (e) {
responseVerse = {
text: `Failed to fetch ${selector} from USFM data provider! Reason: ${e}`,
};
}
}
if (responseVerse.isChanged) {
// Remove any previous edit warning from the beginning of the text so they don't stack
responseVerse.text = responseVerse.text.replace(/^\[.* \d*\] /, '');
return `[${this.editWarning} ${this.editCount}] ${responseVerse.text}`;
}
return responseVerse.text;
}// src\main.ts
async setVerseTextOverride(verseRef: string, verseText: string) {
return this.setInternal(verseRef, { text: verseText, isEdited: true });
}
async getVerseTextOverride(verseRef: string) {
return this.getVerse(verseRef);
}// src\main.ts
// Does nothing, so no need to use `this`
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
async setChapter() {
// We are not supporting setting chapters now, so don't update anything
return false;
}
async getChapter(chapterInfo: [book: string, chapter: number]) {
const [book, chapter] = chapterInfo;
return this.getVerse(`${book} ${chapter}`);
}Inside the activate() function, create an instance of the data provider engine and send it a starter string. Then register this engine with the dataProviders service using a unique ID. Finally, to avoid blocking other activation tasks, await the registration promise at the end of activate().
// src\main.ts
const engine = new QuickVerseDataProviderEngine("editCount =");
const quickVerseDataProviderPromise = papi.dataProviders.registerEngine(
"helloWorld.quickVerse",
engine
);
context.registrations.add(
// previous registrations...
await quickVerseDataProviderPromise
);Now that you are ready to use the new helloWorld.quickVerse data provider, you’ll discard most of the original implementation of the React WebView (with the Button that triggered the DoStuffEvent event) and instead have it display the text of the latest verse. (Unless you do some additional work to make this WebView -- or another one -- that can change the verse reference, this will always be the default verse that is set in main.ts: JHN 11:35.)
Change the imports in hello-world.web-view.tsx to import the useData and useDataProvider hooks from @papi/frontend/react. Declare a const for the data provider using the useDataProvider hook. Next, declare a variable that will get the verse data from the provider using the useData hook. Finally, display the data in a <div>.
Note: When fetching data from a data provider with useData, you don’t have to use useDataProvider unless you need direct access to the provider instance itself. Instead of passing the provider object to useData (as shown in thi example), you can simply pass the data provider ID string.
// src\hello-world.web-view.tsx
import { useData, useDataProvider } from '@papi/frontend/react';
globalThis.webViewComponent = function HelloWorld() {
const extensionVerseDataProvider = useDataProvider('helloWorld.quickVerse');
const [latestExtensionVerseText] = useData<'helloWorld.quickVerse'>(
extensionVerseDataProvider,
).Verse('latest', 'Loading latest Scripture text from Hello World...');
return (
<div className="tw-flex tw-flex-col tw-gap-4 tw-p-6">
<div className="pr-twp tw-text-2xl tw-font-semibold tw-tracking-tight">
Hello World <span className="tw-text-muted-foreground tw-font-normal">React</span>
</div>
<div>
{latestExtensionVerseText}
</div>
</div>
);
};If you run Platform.Bible at this point, it should display the text of John 11:35 as expected. However, you might notice the error in the editor warning you that latestExtensionVerseText might be a PlatformError object. If PAPI returns an error instead of a string (or undefined), React won’t be able to render it directly as a child in JSX. To fix this, add imports for getErrorMessage and isPlatformError from platform-bible-utils and then change the JSX to handle the case where latestExtensionVerseText is a PlatformError.
// src\hello-world.web-view.tsx
import { useData, useDataProvider } from '@papi/frontend/react';
import { getErrorMessage, isPlatformError } from 'platform-bible-utils';
globalThis.webViewComponent = function HelloWorld() {
const extensionVerseDataProvider = useDataProvider('helloWorld.quickVerse');
const [latestExtensionVerseText] = useData<'helloWorld.quickVerse'>(
extensionVerseDataProvider,
).Verse('latest', 'Loading latest Scripture text from Hello World...');
return (
<div className="tw-flex tw-flex-col tw-gap-4 tw-p-6">
<div className="pr-twp tw-text-2xl tw-font-semibold tw-tracking-tight">
Hello World <span className="tw-text-muted-foreground tw-font-normal">React</span>
</div>
<div>
{isPlatformError(latestExtensionVerseText)
? getErrorMessage(latestExtensionVerseText)
: latestExtensionVerseText}
</div>
</div>
);
};Use npm start again. You should now see the WebView displaying verse data from the data provider!
This example covers just the basics: displaying a single verse using the Verse data type. However, your extension already includes other data types like VerseTextOverride and Chapter that enable more complex interactions—such as changing verses (and marking them as edited) or retrieving entire chapters.
We encourage you to explore these additional data types and their corresponding get/set methods to expand your extension’s capabilities. For instance, you might add UI elements to:
- Change the current verse or chapter being displayed
- Edit verse text freely using the
VerseTextOverridedata type, observing how changes are flagged - Fetch and display an entire chapter instead of a single verse
Experimenting with these features will deepen your understanding of how Platform.Bible’s data provider system works and how to build richer, more interactive extensions.
Note that code style and other such documentation is stored in the Paranext wiki and covers all Paranext repositories.