storytoolkit
v0.2.4
Published
Story Toolkit JavaScript library for converting content, translating stories, copying projects, and creating editable drafts.
Maintainers
Readme
storytoolkit
Convert source content, translate or copy existing stories, and create private editable StoryMaps drafts.
ESM · modern browser tooling · TypeScript declarations included
Repository build, consumer-test, and release scripts are listed in the npm command reference.
ArcGIS Online is the default portal, and AI is disabled unless an ai
configuration object is supplied.
npm
npm install storytoolkitDirect browser URL
<script type="module">
import { createStoryToolkit } from
"https://story-maps-explorer.vercel.app/toolkit/latest/index.js";
import { convertToMapTour } from
"https://story-maps-explorer.vercel.app/toolkit/latest/converters.js";
</script>Pin production URL imports to an immutable version such as
/toolkit/v0.2.4/.
Choose a use case
| I want to… | API |
| --- | --- |
| Convert CSV into a Map Tour | convertToMapTour |
| Convert StoryMapJS JSON into a Map Tour | convertStoryMapJsToMapTour |
| Convert StoryMapJS JSON into a Sidecar | convertStoryMapJsToSidecar |
| Convert GPX, KML, route JSON, or CSV into an Express Map | convertToExpressMap |
| Convert PPTX into a Briefing | convertPptxToBriefing |
| Migrate a Classic Story Map | convertClassicStoryMapItem |
| Translate a Story, Briefing, or Frame | translation helpers and drafts.publishTranslation |
| Copy between accounts or portals | orgCopy.analyze and orgCopy.copy |
| Export maps from StoryMaps items as PNG | exportStoryMapImages from storytoolkit/map-export |
| Count words and estimate reading time locally | countWords and estimateReadingTime from storytoolkit/converters |
| Analyze story text without AI | analyzeStoryText from storytoolkit/converters |
| Create a private editable draft | drafts.publish |
See the Developer Guide for focused examples for each workflow.
Estimate reading time without AI
Reading-time analysis is synchronous, deterministic, and makes no network or AI requests. It accepts plain text or an existing word count and uses 200 words per minute by default:
import { estimateReadingTime } from "storytoolkit/converters";
const estimate = estimateReadingTime(articleText);
const label = `${estimate.wordCount} words; roughly ${estimate.estimatedMinutes} min read.`;
const japaneseEstimate = estimateReadingTime(japaneseText, {
locale: "ja",
wordsPerMinute: 400,
});Pass { wordsPerMinute: 250 } as the second argument to use a different
reading speed. Pass a BCP 47 locale to use native language-aware word
segmentation for languages such as Chinese, Japanese, and Thai. Runtimes
without Intl.Segmenter fall back to whitespace counting.
Analyze story text without AI
analyzeStoryText produces a JSON-safe content review synchronously in the
browser or Node.js. It does not use AI, make network requests, or require an
account, token, or API key:
import { analyzeStoryText } from "storytoolkit/converters";
const analysis = analyzeStoryText(storyText, {
locale: "en",
wordsPerMinute: 200,
maxPlaces: 8,
knownPlaces: ["Washington Beltway"],
});
console.log(analysis.tone, analysis.sentiment);
console.log(analysis.readability);
console.log(analysis.languages, analysis.places);Language detection and place candidates use deterministic script, context,
and gazetteer rules. Applications can improve local place detection with
knownPlaces. Place results are candidates for review, not authoritative
metadata:
- Places first match the built-in common-place list and
knownPlacescase-insensitively. It also scores capitalized names when they appear after location words such as “in” or “near” or end in a geographic term such as “County,” “River,” or “Park.” Results are deduplicated and limited bymaxPlaces. - Place detection does not call a gazetteer or geocoder, validate that a place
exists, resolve aliases, or return coordinates. False positives and missed
places are possible. Use
knownPlacesfor application-specific names and an optional geocoder when candidates must be verified.
Complete place-detection sample
import { createStoryToolkit } from "storytoolkit";
import {
analyzeStoryText,
verifyStoryPlaceCandidates,
} from "storytoolkit/converters";
const toolkit = createStoryToolkit({
auth: { getAccessToken: () => appSession.accessToken },
});
const storyText = `
The Mill Creek Watershed runs through Portland, Oregon.
Watershed restoration is improving wildlife habitat and neighborhood access.
Community partners lead watershed restoration projects along Mill Creek.
Portland residents can join restoration events this spring.
`;
const analysis = analyzeStoryText(storyText, {
locale: "en-US",
maxPlaces: 5,
knownPlaces: ["Mill Creek Watershed", "Portland", "Oregon"],
});
console.log(JSON.stringify({
wordCount: analysis.wordCount,
estimatedMinutes: analysis.estimatedMinutes,
places: analysis.places,
}, null, 2));
const verified = await verifyStoryPlaceCandidates(analysis.places, {
minScore: 90,
geocode: place => toolkit.geocoding.findAddressCandidates({
singleLine: place,
maxLocations: 1,
outFields: ["Addr_type", "PlaceName", "City", "Region", "Country"],
forStorage: false,
}),
});
console.log(verified.places); // Safe to display as geocoder-confirmed candidates.
console.log(verified.rejected); // Local candidates that did not pass verification.Current deterministic output:
{
"wordCount": 34,
"estimatedMinutes": 1,
"places": ["Mill Creek Watershed", "Portland", "Oregon"]
}verifyStoryPlaceCandidates rejects common coordinate-system terms before
calling the geocoder, then retains only results scoring at least 90 whose
Addr_type represents a geographic feature and whose matched address contains
the original candidate. It preserves the author's original label. The
geocoder callback above requires the toolkit's current ArcGIS access token and
uses forStorage: false; the helper itself does not store results.
Tone, sentiment, and Flesch readability currently support English and include
supported, confidence, and method fields so clients can distinguish
heuristics from authoritative metadata. Unsupported languages return explicit
warnings instead of English-only scores.
Quick start: CSV to Map Tour
This complete flow converts local CSV, checks the review report, and creates a new private draft. Local conversion does not require authentication; draft creation does.
import { createStoryToolkit } from "storytoolkit";
import { convertToMapTour } from "storytoolkit/converters";
const toolkit = createStoryToolkit({
auth: {
// Resolve this from your application's existing auth/session layer.
getAccessToken: () => appSession.accessToken,
},
});
const result = convertToMapTour({
text: `title,description,latitude,longitude
Library,Community library,34.0565,-117.1957`,
sourceName: "places.csv",
draft: {
title: "Community places",
byline: "Planning team",
imagePolicy: "hide",
},
});
if (!result.ok || !result.value) {
throw new Error(result.report.summary);
}
const created = await toolkit.drafts.publish({
type: "story",
draftJson: result.value.build.draft,
itemResources: result.value.build.itemResources,
metadata: {
title: "Community places",
summary: "Converted from CSV.",
byline: "Planning team",
tags: ["Map Tour"],
},
});
console.log(created.builderUrl);drafts.publish always creates a new private, unpublished item. It does not
publish publicly, update an existing item, or delete existing content.
Created items include the storytoolkit-app ArcGIS type keyword and a detected
storytoolkit-converter-<blocktype> keyword for tracking in Explorer or ArcGIS
item searches.
Export maps from StoryMaps items as PNG
loadStoryMapExportInventory evaluates every authored 2D map occurrence in a
StoryMaps item, including Stories, Briefings, and Frames. Pass that inventory
to exportStoryMapImages to render a selected set without fetching the item
again. Web Maps, Express Maps, and one overview for each Map Tour are supported.
The result contains the PNG Blobs,
individual failures, fidelity warnings, and a provenance manifest suitable for
an archive. Use getStoryMapExportSourceKey to group repeated occurrences of
the same Web Map item, Express Map resource, or Map Tour map. Pass selected
descriptor node IDs through includeNodeIds to export whole groups or individual
authored views. The manifest records both selected and skipped map occurrences.
Resource-backed Express Map picture markers and marker data found on inline Map
Tour geometries are embedded as base64 ArcGIS picture-marker symbols. Missing,
inaccessible, or oversized marker resources fall back to the standard marker
and add a fidelity warning without interrupting the remaining exports.
import {
exportStoryMapImages,
getStoryMapExportSourceKey,
loadStoryMapExportInventory,
} from "storytoolkit/map-export";
const inventory = await loadStoryMapExportInventory({
story: "https://www.arcgis.com/home/item.html?id=ITEM_ID",
portalUrl: "https://www.arcgis.com",
token: appSession?.accessToken,
});
const selectedSourceKeys = new Set(["webmap:WEB_MAP_ITEM_ID"]);
const selectedNodeIds = inventory.descriptors
.filter(descriptor => selectedSourceKeys.has(getStoryMapExportSourceKey(descriptor)))
.map(descriptor => descriptor.nodeId);
const result = await exportStoryMapImages({
story: "https://www.arcgis.com/home/item.html?id=ITEM_ID",
portalUrl: "https://www.arcgis.com",
token: appSession?.accessToken, // Optional for public content.
inventory,
includeKinds: ["webmap", "expressmap", "maptour"],
includeNodeIds: selectedNodeIds,
width: 1600,
height: 1067,
dpi: 200,
});
for (const image of result.images) {
console.log(image.fileName, image.blob, image.warnings);
}
console.log(result.failures, result.manifest);Tokens are sent only to the selected portal, its ArcGIS service family, and hosts listed by that portal as trusted. They are never written to the export manifest. The host application remains responsible for token refresh and sign-out.
Resource handling
Converters return both the draft graph and the files it references:
const { draft, itemResources } = result.value.build;
await toolkit.drafts.publish({
type: "story",
draftJson: draft,
itemResources,
metadata,
});draftJson.resources contains logical references. itemResources contains
the text or Blob payloads uploaded with the new ArcGIS item. Publishing also
externalizes supported inline JSON and Express Map data, uploads the supplied
resources, and writes the final draft.json.
StoryMaps quiz block JSON
Quiz blocks use the same node subtree in long-form Stories and Frames. The containing Story or Frame panel references the quiz node; the quiz references its questions in authored order.
{
"n-quiz": {
"type": "quiz",
"children": ["n-question-1"],
"config": {
"resultScreen": {
"title": "Thank you for taking this quiz.",
"description": "Optional completion text",
"shouldShowRestartButton": true
}
}
},
"n-question-1": {
"type": "quiz-question",
"data": {
"type": "single-select",
"question": "Question text",
"answers": [
{
"id": "answer-1",
"text": "Choice one",
"explanation": { "text": "Optional explanation" }
},
{ "id": "answer-2", "text": "Choice two" }
],
"correctAnswer": "answer-1",
"hint": { "text": "Optional hint" }
}
}
}The current contract supports single-select questions, up to 10 questions
per quiz, two to four answers per question, one correctAnswer id, and up to
240 visible characters in authored text fields. config, resultScreen,
hint, and answer explanation are optional. Question, answer, hint, and
explanation strings may contain StoryMaps rich-text HTML. The observed payload
does not define points, weights, randomization, required-answer behavior,
attempt limits, media, or analytics.
Converters can import STORYMAPS_QUIZ_LIMITS, the quiz node types and guards,
collectStoryMapsQuizBlocks, and collectStoryMapsQuizTextFields from
storytoolkit/converters. Graph consumers should discover quiz nodes through
container children relationships rather than assuming a direct Story-root
parent. Story-to-Frame preserves the complete quiz subtree, and translation
collects all authored quiz and result-screen text while leaving answer ids and
correct-answer references unchanged.
Markdown and article converters can build an AI-grounding source with
createMarkdownQuizSource, request structured questions using
createMarkdownQuizGenerationContext, validate the returned ArcGIS AI value
with parseGeneratedMarkdownQuiz, and add the resulting nodes with
appendGeneratedQuizToStoryDraft. Insertion is deterministic, occurs before
credits, escapes generated text, and does not mutate the source draft. The host
application remains responsible for invoking its AI provider and requiring
author review.
Only ArcGIS-supported item-resource formats can be uploaded successfully.
Convert unsupported source files before adding them to itemResources, and
update the draft reference to the converted file. Check the ArcGIS REST API
addResources documentation
for the current format and upload limits. A storable format must also be
renderable by the StoryMaps block that references it.
Use toolkit.items.hydrateResources(...) to download known resources from an
existing item before publishing them to a new item. It returns hydrated
resources plus warnings for files it could not read; it does not discover
resource references. Protected reads need source authentication, and uploads
need destination authentication. See the
Resources section
for the resource contract, copying example, and failure behavior.
Bring your own authentication
Story Toolkit does not require a particular sign-in library or credential store. Use whichever path already fits the application:
- Adapt an
ArcGISIdentityManagerfrom ArcGIS REST JS. - Supply an access token obtained through the ArcGIS REST API or ArcGIS Maps SDK for JavaScript.
- Use the optional browser session and OAuth helpers exported from
storytoolkit/browser.
For an existing token, provide:
const auth = {
getAccessToken: () => appSession.accessToken,
getSession: () => ({
token: appSession.accessToken,
username: appSession.username,
expiresAt: appSession.expiresAt,
}),
};getSession is optional and avoids a username lookup during draft creation.
The host application owns token refresh, persistence, and sign-out.
Existing ArcGIS REST JS applications can adapt their
ArcGISIdentityManager without changing their authentication workflow:
const portalRestUrl = "https://www.arcgis.com/sharing/rest";
const getAccessToken = () => authManager.getToken(portalRestUrl);
const toolkit = createStoryToolkit({
auth: {
getAccessToken,
getSession: async () => ({
token: await getAccessToken(),
username: await authManager.getUsername(),
expiresAt: authManager.tokenExpires.getTime(),
}),
},
});Optional session-storage and OAuth adapters are available from
storytoolkit/browser for applications that want them:
import {
createArcGISBrowserOAuthAdapter,
createArcGISSessionAuthStore,
createSessionStorageAdapter,
} from "storytoolkit/browser";
const storage = createSessionStorageAdapter();
const auth = createArcGISSessionAuthStore({ storage });
const oauth = createArcGISBrowserOAuthAdapter({
portalUrl,
clientId,
authStore: auth,
});Pass auth and storage to createStoryToolkit, subscribe to the OAuth
adapter, and call oauth.openSignIn() from the host interface. These helpers
are not imported by the core package.
TypeScript and editor support
TypeScript declarations and declaration maps are bundled for every public
entry point. No @types package is needed. TypeScript-aware editors provide
autocomplete, parameter hints, hover details, and definition navigation in
TypeScript and JavaScript.
JavaScript projects can add // @ts-check for inline diagnostics:
// @ts-check
import { createStoryToolkit } from "storytoolkit";
/** @type {import("storytoolkit").StoryMapsToolkitConfig} */
const config = {
};
const toolkit = createStoryToolkit(config);Entry points
The npm package is ESM-only. Core and converter entry points do not import Web
Storage APIs. Optional session and OAuth helpers are isolated in
storytoolkit/browser.
| Entry point | Use it for |
| --- | --- |
| storytoolkit | Configured portal access, services, copying, and private drafts |
| storytoolkit/converters | Map Tour, Express Map, PPTX, translation, and other conversions |
| storytoolkit/classic-converter | Supported Classic Story Map migration |
| storytoolkit/story-package | Story Package creation, validation, and compilation |
| storytoolkit/items | Pure item, URL, search, type, and display helpers |
| storytoolkit/map-export | StoryMaps item map inventory and print-service PNG export |
| storytoolkit/browser | Optional browser session storage and OAuth |
Applications without a package manager can use the hosted browser ESM modules.
Pin production imports to /toolkit/v0.2.4/; /toolkit/latest/ follows the
deployed build.
Licensed under the MIT License.
