diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff30dc..2664619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to `@webspellchecker/wproofreader-learnosity`. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2.0.0] + +### Breaking + +- **`bundleUrl` option removed.** To point at a self-hosted or non-default + bundle, pass the WProofreader SDK's own `srcUrl` inside the `wproofreader` + block instead of the top-level `bundleUrl`: + + ```js + // before + LearnosityWProofreader.init({ wproofreader: { serviceHost: '...' }, bundleUrl: '...' }); + // after + LearnosityWProofreader.init({ wproofreader: { serviceHost: '...', srcUrl: '...' } }); + ``` + +### Added + +- **Named export.** The plugin can now be imported as + `import { LearnosityWProofreader } from '@webspellchecker/wproofreader-learnosity'` + in addition to the existing default import, mirroring the WProofreader SDK + (default plus named exports). Both resolve to the same object. + +### Changed + +- **Bundle loading delegated to the WProofreader SDK.** The plugin now uses + `@webspellchecker/wproofreader-sdk-js` to load the WProofreader bundle and + create instances, replacing the hand-rolled script injection and global + polling. The internal `window.WEBSPELLCHECKER_CONFIG` object is no longer set; + per-editor options are passed straight to the SDK's `init()` (so the SDK's + `srcUrl`, `lang`, etc. ride through the `wproofreader` block). The default + bundle URL is unchanged. + ## [1.2.1] ### Features @@ -43,6 +75,7 @@ Initial public release. - **Configurable WProofreader bundle URL** via `bundleUrl`, for customers running WProofreader from a non-default CDN host. - **Apache-2.0 licensed.** +[2.0.0]: https://github.com/WebSpellChecker/wproofreader-learnosity/releases/tag/v2.0.0 [1.2.1]: https://github.com/WebSpellChecker/wproofreader-learnosity/releases/tag/v1.2.1 [1.2.0]: https://github.com/WebSpellChecker/wproofreader-learnosity/releases/tag/v1.2.0 [1.1.0]: https://github.com/WebSpellChecker/wproofreader-learnosity/releases/tag/v1.1.0 diff --git a/README.md b/README.md index 3b63f6b..53af17d 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,9 @@ WProofreader for Learnosity. Spelling, grammar and style assistance inside Learnosity rich-text widgets, with one function call. -> v1.2.1. Apache-2.0. TypeScript definitions included. Two install paths: npm (ESM) and CDN (IIFE). Cloud and self-hosted WProofreader. Per-editor surface filter, lifecycle cleanup, locale-aware defaults, optional escape hatches for unusual host apps. - ## Install -The connector offers two install paths. The runtime API is identical, `LearnosityWProofreader.init({...})` in both. +The plugin offers two install paths. The runtime API is identical, `LearnosityWProofreader.init({...})` in both. ### Via npm (Node host apps) @@ -15,19 +13,25 @@ npm install @webspellchecker/wproofreader-learnosity ``` ```js -import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity'; +import { LearnosityWProofreader } from '@webspellchecker/wproofreader-learnosity'; LearnosityWProofreader.init({ wproofreader: { serviceId: 'YOUR_WPROOFREADER_SERVICE_ID' } }); ``` +The plugin is available as both a named and a default export, so a default import works too: + +```js +import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity'; +``` + ### Via ` + ``` -The bundle exposes a single global, `window.LearnosityWProofreader`, with the same `init` method. About 5 KB minified, no runtime dependencies. jsDelivr serves from npm, so the pinned URL above resolves once the package is published. - ## Quickstart ```js -import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity'; +import { LearnosityWProofreader } from '@webspellchecker/wproofreader-learnosity'; // Before Learnosity widgets render. LearnosityWProofreader.init({ wproofreader: { serviceId: 'YOUR_WPROOFREADER_SERVICE_ID' } }); -// Initialise Learnosity as normal. The connector attaches to longtextV2 editors automatically. +// Initialise Learnosity as normal. The plugin attaches to longtextV2 editors automatically. LearnosityApp.init(signedRequest, { readyListener() { console.log('Learnosity ready'); }, errorListener(err) { console.error(err); } @@ -76,9 +78,9 @@ LearnosityWProofreader.init({ serviceProtocol: 'https', serviceHost: 'localhost', servicePort: 443, - servicePath: '/wscservice/api' - }, - bundleUrl: 'https://localhost/wscservice/wscbundle/wscbundle.js' + servicePath: '/wscservice/api', + srcUrl: 'https://localhost/wscservice/wscbundle/wscbundle.js' + } }); ``` @@ -86,7 +88,7 @@ Either `wproofreader.serviceId` or `wproofreader.serviceHost` must be set; other ## Configuration -`init()` takes a single options object with two groups: a nested `wproofreader` sub-object (forwarded to WProofreader unchanged) and connector-only fields at the top level. +`init()` takes a single options object with two groups: a nested `wproofreader` sub-object (forwarded to WProofreader unchanged) and plugin-only fields at the top level. ### WProofreader options (under `wproofreader`) @@ -99,39 +101,26 @@ These pass straight through to WProofreader's runtime config. The tables below c | `serviceHost` | _none_ | Self-hosted: WProofreader server hostname. Presence of `serviceHost` (or `serviceId`) is what activates `init()`. | | `servicePort` | _none_ | Self-hosted: port. | | `servicePath` | _none_ | Self-hosted: URL path to the WProofreader API. | +| `srcUrl` | _SDK default (cloud CDN)_ | SDK option: where to load `wscbundle.js` from. Override for a self-hosted deployment or non-default CDN; defaults to the WebSpellChecker cloud bundle. | | `enableGrammar` | `true` | Grammar checking on top of spell checking. | | `autocorrect` | `true` | Suggested replacement on typo accept. | | `lang` | host page's ``, or `en_US`| Read from `` with `-` normalised to `_` (e.g. `en-US` → `en_US`). Falls back to `en_US` if the page does not set a `lang` or sets a bare un-regioned one (`en`, `fr`), because WProofreader expects region-qualified codes. Pass explicitly to override. | If neither `wproofreader.serviceId` nor `wproofreader.serviceHost` is set, `init()` no-ops with a console warning so the host app can call it unconditionally. -### Connector options (top level) + +### Plugin options (top level) | Option | Default | Notes | |-------------------|---------------|--------------------------------------------------------| | `surfaces` | `['author']` | Subset of `['questions','items','author']`. **Per-editor** filter: each editor's ancestry is walked for surface-specific markers (`lrn-author*` → author, `lrn_player*` → items, otherwise → questions); editors whose surface is not in the allowed list are skipped. Default is Author only, for assessment integrity. Opt in to assessment proofreading explicitly with `['questions', 'items', 'author']`. | | `enableShorttext` | `false` | Also attach to `shorttext` inputs (``) inside Learnosity scope. Off by default because plain text inputs are usually short and proofreading them is noisy. | | `customSelectors` | `[]` | Extra ancestor CSS selectors that count as "inside Learnosity scope" for unusual host apps. Each entry is matched via `element.matches`. | -| `bundleUrl` | (default CDN) | Override the WProofreader bundle URL for non-default CDNs or self-hosted servers. | - -### Examples ```js -// Authoring-only (the default). No-ops on assessment pages. -LearnosityWProofreader.init({ - wproofreader: { serviceId: '...' } -}); - -// All three surfaces, including assessment. The host app should derive this -// from server-side activity metadata so it cannot be flipped from the console. -LearnosityWProofreader.init({ - wproofreader: { serviceId: '...' }, - surfaces: ['questions', 'items', 'author'] -}); - -// Unusual host app that wraps Learnosity inside its own container class. LearnosityWProofreader.init({ wproofreader: { serviceId: '...' }, + surfaces: ['questions', 'items', 'author'], customSelectors: ['.my-host-app-learnosity-region'] }); ``` @@ -148,25 +137,15 @@ LearnosityWProofreader.init({ ## Supported Learnosity versions -Tested manually against the `latest-lts` channel. The connector tracks editor placement via the `lrn`-prefixed class convention plus a small per-surface marker set; class-rename changes between LTS releases are an isolated update in `src/index.js`. +Tested manually against the `latest-lts` channel. The plugin tracks editor placement via the `lrn`-prefixed class convention plus a small per-surface marker set; class-rename changes between LTS releases are an isolated update in `src/index.js`. ## Documentation and support - **WProofreader product page**: -- **Configuration reference** (every WProofreader option beyond what this connector documents): -- **WProofreader SDK source** (the underlying library the connector wraps): +- **Configuration reference** (every WProofreader option beyond what this plugin documents): - **Self-hosted server image**: - **Contact and commercial enquiries**: -## Tests - -```bash -npm install -npm test -``` - -`npm test` runs the unit suite under `node --test`. - ## License Apache-2.0. See [LICENSE](./LICENSE). diff --git a/package-lock.json b/package-lock.json index c43eb28..bf42dc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,16 @@ { "name": "@webspellchecker/wproofreader-learnosity", - "version": "1.2.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@webspellchecker/wproofreader-learnosity", - "version": "1.2.0", + "version": "2.0.0", "license": "Apache-2.0", + "dependencies": { + "@webspellchecker/wproofreader-sdk-js": "^1.1.2" + }, "devDependencies": { "esbuild": "^0.23.0" }, @@ -423,6 +426,12 @@ "node": ">=18" } }, + "node_modules/@webspellchecker/wproofreader-sdk-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@webspellchecker/wproofreader-sdk-js/-/wproofreader-sdk-js-1.1.2.tgz", + "integrity": "sha512-s6pEmGr5Y0k7p2JbG0mqNka4thzx0rbowaGlIXWp5UAj/W54NU73AxyIktPQF8Hzouxnsds1T323933Q6AZPVw==", + "license": "Apache-2.0" + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", diff --git a/package.json b/package.json index 56dd0ef..acaeb9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@webspellchecker/wproofreader-learnosity", - "version": "1.2.1", + "version": "2.0.0", "description": "WProofreader for Learnosity. Spelling, grammar and style assistance inside Learnosity rich-text widgets.", "type": "module", "main": "src/index.js", @@ -13,16 +13,18 @@ }, "files": [ "src/", - "dist/", - "README.md", - "LICENSE", - "CHANGELOG.md" + "dist/" ], "scripts": { - "test": "node --test tests/*.test.js", - "build": "esbuild src/index.js --bundle --format=iife --global-name=LearnosityWProofreader '--footer:js=LearnosityWProofreader=LearnosityWProofreader.default;' --minify --outfile=dist/wproofreader-learnosity.min.js", + "test": "node --test --import ./tests/register-stubs.mjs tests/*.test.js", + "build": "npm run build:iife && npm run build:esm", + "build:iife": "esbuild src/index.js --bundle --format=iife --global-name=LearnosityWProofreader '--footer:js=LearnosityWProofreader=LearnosityWProofreader.LearnosityWProofreader;' --minify --outfile=dist/wproofreader-learnosity.min.js", + "build:esm": "esbuild src/index.js --bundle --format=esm --minify --outfile=dist/wproofreader-learnosity.esm.js", "prepack": "npm run build" }, + "dependencies": { + "@webspellchecker/wproofreader-sdk-js": "^1.1.2" + }, "devDependencies": { "esbuild": "^0.23.0" }, diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..d7e1e4d --- /dev/null +++ b/src/config.js @@ -0,0 +1,65 @@ +/** + * Pure configuration logic for the connector: framework- and DOM-agnostic + * enough to unit-test in isolation. Nothing here loads scripts or touches the + * WProofreader SDK. + */ + +// Surfaces the connector recognises, and the `window` global Learnosity +// exposes for each (used for the page-level gate and per-editor detection). +export const ALLOWED_SURFACES = ['questions', 'items', 'author']; +export const SURFACE_GLOBALS = { + questions: 'LearnosityApp', + items: 'LearnosityItems', + author: 'LearnosityAuthor' +}; +// Author only by default: Questions and Items render student-facing +// assessments where spell-check can compromise the activity. Host apps opt in +// explicitly with `surfaces: ['questions', 'items', 'author']`. +export const DEFAULT_SURFACES = ['author']; + +// Fixed partner tag so WebSpellChecker can attribute traffic to this plugin +// (cf. the CKEditor 5 plugin's `proofreader_ck5`). Not caller-configurable. +export const APP_TYPE = 'wpr_learnosity'; + +/* + * Split options into the WProofreader block (forwarded to the SDK's init() + * unchanged; spread then default so unknown/self-hosted options — including the + * SDK's own `srcUrl` for the bundle location — flow through while our defaults + * apply only when unset) and connector-only fields. `appType` is pinned to the + * fixed partner tag (set after the spread) so a caller cannot override it. + */ +export function buildConfig(options) { + const wp = options.wproofreader || {}; + return { + wproofreader: { + ...wp, + enableGrammar: wp.enableGrammar !== false, + autocorrect: wp.autocorrect !== false, + lang: wp.lang || defaultLang(), + appType: APP_TYPE + }, + surfaces: normalizeSurfaces(options.surfaces), + enableShorttext: options.enableShorttext === true, + customSelectors: Array.isArray(options.customSelectors) ? options.customSelectors.slice() : [] + }; +} + +/* + * Default language from the host page's ``, with `-` normalised to + * `_` (e.g. `en-US` → `en_US`). WProofreader rejects bare codes like `en`, so + * we fall back to `en_US` unless the page lang is region-qualified. + */ +export function defaultLang() { + if (typeof document === 'undefined') return 'en_US'; + const html = document.documentElement; + if (!html || !html.lang) return 'en_US'; + const lang = html.lang; + if (lang.indexOf('-') === -1 && lang.indexOf('_') === -1) return 'en_US'; + return lang.replace('-', '_'); +} + +// Valid subset of ALLOWED_SURFACES; non-arrays fall back to the default. +export function normalizeSurfaces(input) { + if (!Array.isArray(input)) return DEFAULT_SURFACES.slice(); + return input.filter(function (s) { return ALLOWED_SURFACES.indexOf(s) !== -1; }); +} diff --git a/src/index.d.ts b/src/index.d.ts index 63d2d01..5e9a1fe 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -3,104 +3,32 @@ * Hand-written to match src/index.js. Keep in sync. */ -/** - * Learnosity API surfaces the connector knows how to scope to. Each editor's - * ancestry is walked for surface-specific markers; editors whose surface is - * not in the allowed list are skipped. - */ export type Surface = 'questions' | 'items' | 'author'; -/** - * Runtime options forwarded to WProofreader unchanged. The connector does - * not enforce any schema beyond `serviceId` / `serviceHost` activation, so - * any other WProofreader option may also be set. - */ export interface WProofreaderOptions { - /** - * WProofreader Cloud service ID. Domain-locked server-side. Required for - * the Cloud path. Omit when using a self-hosted deployment. - */ serviceId?: string; - - /** Self-hosted: protocol. */ serviceProtocol?: 'http' | 'https'; - - /** - * Self-hosted: WProofreader server hostname. Presence of `serviceHost` (or - * `serviceId`) activates `init()`. - */ serviceHost?: string; - - /** Self-hosted: port. */ servicePort?: number | string; - - /** Self-hosted: URL path to the WProofreader API (e.g. `/wscservice/api`). */ servicePath?: string; - - /** Default `true`. */ enableGrammar?: boolean; - - /** Default `true`. */ autocorrect?: boolean; - - /** - * Default: the host page's `` attribute, with `-` normalised to - * `_` (e.g. `en-US` becomes `en_US`). Falls back to `en_US` if the page - * does not set a `lang` or sets a bare un-regioned one (WProofreader - * requires region-qualified codes). - */ lang?: string; - - /** - * Forward-compatible escape hatch: any other property is passed through to - * WProofreader's runtime config unchanged. Useful for WProofreader options - * the connector does not enumerate explicitly. - */ + /** SDK option: WProofreader bundle URL, for self-hosted or non-default CDN deployments. */ + srcUrl?: string; + // Note: `appType` is set by the plugin to a fixed partner tag and is not caller-configurable. [other: string]: unknown; } export interface InitOptions { - /** WProofreader runtime options. Forwarded to WProofreader unchanged. */ wproofreader?: WProofreaderOptions; - - /** - * Subset of `['questions','items','author']`. Default `['author']` for - * assessment integrity. The connector walks each editor's ancestry to - * detect which Learnosity API placed it (`lrn-author*` → author, - * `lrn_player*` → items, otherwise → questions) and skips editors whose - * surface is not in the allowed list. Opt in to assessment proofreading - * explicitly with `['questions', 'items', 'author']`. - */ surfaces?: ReadonlyArray; - - /** - * Default `false`. Also attach to `shorttext` inputs (``) - * inside Learnosity scope. - */ enableShorttext?: boolean; - - /** - * Extra ancestor CSS selectors that count as "inside Learnosity scope" for - * unusual host apps. Each entry is matched via `element.matches`. - */ customSelectors?: ReadonlyArray; - - /** Override the WProofreader bundle URL for non-default CDNs or self-hosted servers. */ - bundleUrl?: string; } -/** - * Public API surface. The default export is the namespace object; the call - * site mirrors that shape. - */ -export interface LearnosityWProofreader { - /** - * Initialise the connector. Should be called before Learnosity widgets - * render. Safe to call multiple times: subsequent calls update the live - * configuration without reloading the WProofreader bundle. - */ - init(options?: InitOptions): void; +export declare class LearnosityWProofreader { + static init(options?: InitOptions): void; } -declare const LearnosityWProofreader: LearnosityWProofreader; export default LearnosityWProofreader; diff --git a/src/index.js b/src/index.js index 852f08b..ce4b826 100644 --- a/src/index.js +++ b/src/index.js @@ -1,502 +1,231 @@ /** * WProofreader connector for Learnosity. * - * Targeted attachment to Learnosity-rendered editables, scoped by an ancestry - * walk for any `lrn`-prefixed class plus optional caller-supplied selectors. - * Per-editor `surfaces` filter (default `['author']`) covers assessment - * integrity by excluding Questions and Items by default; the host app opts in - * explicitly. Cleans up WProofreader instances when their host elements are - * removed from the DOM (single-page apps that rebuild Learnosity widgets do - * not leak). + * Attaches WProofreader to Learnosity-rendered editables only, scoped by an + * ancestry walk for `lrn`-prefixed classes (plus optional caller selectors), + * and filtered per editor by Learnosity surface. Bundle loading and instance + * creation are delegated to the WProofreader SDK; this class only decides which + * editors to attach to and cleans them up when they leave the DOM. * - * Usage in a host app: - * - * import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity'; - * - * LearnosityWProofreader.init({ - * wproofreader: { serviceId: 'YOUR_WPROOFREADER_SERVICE_ID' } - * }); - * - * LearnosityApp.init(signedRequest, callbacks); - */ - -const DEFAULT_BUNDLE_URL = - 'https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js'; - -/* - * Surfaces the connector recognises. `SURFACE_GLOBALS` maps each to the - * `window` global Learnosity exposes for that API. Used in two places: - * - `isAnyAllowedSurfaceLoaded`: cheap page-level gate; skip everything if - * no allowed-surface global has loaded yet. - * - `detectSurface`: per-editor classification via DOM-ancestor markers, - * so multi-API pages are filtered correctly. - */ -const ALLOWED_SURFACES = ['questions', 'items', 'author']; -const SURFACE_GLOBALS = { - questions: 'LearnosityApp', - items: 'LearnosityItems', - author: 'LearnosityAuthor' -}; -/* - * Default surfaces. Author only, because Questions and Items render - * student-facing assessments where spell-check can compromise the activity - * itself. Host apps that want proofreading in those contexts opt in - * explicitly with `surfaces: ['questions', 'items', 'author']`. + * import { LearnosityWProofreader } from '@webspellchecker/wproofreader-learnosity'; + * LearnosityWProofreader.init({ wproofreader: { serviceId: '...' } }); */ -const DEFAULT_SURFACES = ['author']; -/* - * Maximum wait for `window.WEBSPELLCHECKER` to appear after the bundle - * `