npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

storytoolkit

v0.2.4

Published

Story Toolkit JavaScript library for converting content, translating stories, copying projects, and creating editable drafts.

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 storytoolkit

Direct 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 knownPlaces case-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 by maxPlaces.
  • 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 knownPlaces for 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:

  1. Adapt an ArcGISIdentityManager from ArcGIS REST JS.
  2. Supply an access token obtained through the ArcGIS REST API or ArcGIS Maps SDK for JavaScript.
  3. 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.