diff --git a/.github/workflows/test-workspace-notes.yml b/.github/workflows/test-workspace-notes.yml new file mode 100644 index 0000000..b6c5c9a --- /dev/null +++ b/.github/workflows/test-workspace-notes.yml @@ -0,0 +1,70 @@ +name: Test blockly-workspace-notes + +on: + pull_request: + workflow_dispatch: + +jobs: + quality: + name: Lint and format + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + working-directory: blockly-workspace-notes + run: npm ci + + - name: Lint + working-directory: blockly-workspace-notes + run: npm run lint + + - name: Check formatting + working-directory: blockly-workspace-notes + run: npm run format:check + + # `build:types`, not `typecheck`: `tsc --noEmit` returns before + # declaration diagnostics run, so it cannot see the TS4xxx errors that + # declaration emit surfaces. + - name: Typecheck + working-directory: blockly-workspace-notes + run: npm run build:types + + test-workspace-notes: + name: Test on Node ${{ matrix.node }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # The minimum supported version from package.json engines, plus the + # current LTS releases. Node 18 is excluded deliberately: it is past + # end of life, and jsdom (which blockly/core-node.js pulls in for XML + # handling) now reaches an ES module through require(), which only + # works from Node 20.19 onwards. + node: [20, 22, 24] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Install dependencies + working-directory: blockly-workspace-notes + run: npm ci + + - name: Run blockly-workspace-notes tests + working-directory: blockly-workspace-notes + run: npm run test + + - name: Build + working-directory: blockly-workspace-notes + run: npm run build diff --git a/README.md b/README.md index a7ebc88..dec3576 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,4 @@ Plugins for [Blockly](https://github.com/google/blockly) based on code by [MIT App Inventor](https://github.com/mit-cml/appinventor-sources) * [Lexical Variables](./block-lexical-variables) +* [Workspace Notes](./blockly-workspace-notes) diff --git a/blockly-workspace-notes/.prettierignore b/blockly-workspace-notes/.prettierignore new file mode 100644 index 0000000..2630d31 --- /dev/null +++ b/blockly-workspace-notes/.prettierignore @@ -0,0 +1,5 @@ +build/ +dist/ +node_modules/ +package-lock.json +docs/images/ diff --git a/blockly-workspace-notes/.prettierrc.json b/blockly-workspace-notes/.prettierrc.json new file mode 100644 index 0000000..a68f41b --- /dev/null +++ b/blockly-workspace-notes/.prettierrc.json @@ -0,0 +1,14 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "preserve", + "trailingComma": "all", + "bracketSpacing": false, + "bracketSameLine": false, + "arrowParens": "always", + "proseWrap": "preserve", + "endOfLine": "lf" +} diff --git a/blockly-workspace-notes/README.md b/blockly-workspace-notes/README.md new file mode 100644 index 0000000..966554b --- /dev/null +++ b/blockly-workspace-notes/README.md @@ -0,0 +1,70 @@ +# Workspace Notes + +[![Built on Blockly](https://tinyurl.com/built-on-blockly)](https://github.com/google/blockly) + +Sticky notes for a Blockly workspace: draggable, resizable, colour-coded cards +with a title, an author and a stacking order. + +

+ The parts of a note: title bar, collapse and delete buttons, title, body, author and date, and resize handle; and below it a locked note with a padlock, no delete button and no resize handle +

+ +Notes extend Blockly's own workspace comments, so dragging, resizing, +selection, keyboard navigation and undo all work exactly as they already do. +They are saved alongside your blocks, and round-trip through both JSON and XML. + +## Install + +```bash +npm install @mit-app-inventor/blockly-workspace-notes +``` + +Needs Blockly 13.2.1 or newer as a peer dependency, and Node 20+ to build. + +## Use + +```js +import * as Blockly from 'blockly'; +import {WorkspaceNotes} from '@mit-app-inventor/blockly-workspace-notes'; + +const workspace = Blockly.inject('blocklyDiv', {toolbox}); +new WorkspaceNotes(workspace).init(); +``` + +That is the whole setup. Right-click the workspace to add a note; click a +note's title to rename it; right-click a note to recolour, pin, collapse or +restack it. + +> **Import the plugin before `Blockly.inject`.** Blockly's stylesheets only +> reach workspaces injected after they are registered. + +## Documentation + +- [Getting started](./docs/getting-started.md) — install, setup and options +- [Using notes](./docs/using-notes.md) — everything you can do with a note +- [API](./docs/api.md) — the exported classes and functions +- [Saving and loading](./docs/saving.md) — the save format, JSON and XML +- [Design](./docs/design.md) — why it works this way, and how the source is + laid out + +## Develop + +```bash +git clone https://github.com/mit-cml/blockly-plugins.git +cd blockly-plugins/blockly-workspace-notes +npm install +npm start +``` + +`npm start` opens the playground. Other scripts: + +```bash +npm test # typecheck, then the mocha suites +npm run build # bundle and .d.ts into dist/ +npm run lint # ESLint +npm run format # Prettier +``` + +## License + +Apache-2.0 diff --git a/blockly-workspace-notes/docs/README.md b/blockly-workspace-notes/docs/README.md new file mode 100644 index 0000000..b2d11ca --- /dev/null +++ b/blockly-workspace-notes/docs/README.md @@ -0,0 +1,11 @@ +# Documentation + +- **[Getting started](./getting-started.md)** — install it, switch it on, and + the options you can pass. +- **[Using notes](./using-notes.md)** — what a note is and everything you can + do with one. +- **[API](./api.md)** — the classes and functions the package exports. +- **[Saving and loading](./saving.md)** — what a saved note looks like, in JSON + and in XML. +- **[Design](./design.md)** — why a note works the way it does, and how the + source is laid out. diff --git a/blockly-workspace-notes/docs/api.md b/blockly-workspace-notes/docs/api.md new file mode 100644 index 0000000..c3d1368 --- /dev/null +++ b/blockly-workspace-notes/docs/api.md @@ -0,0 +1,86 @@ +# API + +Most apps only need `WorkspaceNotes`. The rest is here for apps that drive +saving and loading themselves. + +## WorkspaceNotes + +```js +const notes = new WorkspaceNotes(workspace, options); +``` + +| Member | What it does | +| -------------------- | --------------------------------------------------- | +| `init()` | Starts the plugin. Safe to call twice. | +| `dispose()` | Stops it and restores everything Blockly had before | +| `createNote(state?)` | Adds a note, in front, as one undo step | +| `getNotes()` | Every note on the workspace | + +```js +notes.createNote({title: 'TODO', text: 'Check the loop', colour: '#f9bbc5'}); +notes.getNotes().filter((note) => note.isPinned()); +``` + +## A note + +`Note` is a note on a rendered workspace; `NoteComment` is the same thing +headless, which is what you get on a workspace with no renderer. Both carry the +same note-specific methods on top of Blockly's own comment API. + +| Method | What it does | +| ------------------------------ | -------------------------------------- | +| `getTitle()` / `setTitle(s)` | The heading | +| `getColour()` / `setColour(c)` | Any CSS colour; stored as hex | +| `isPinned()` / `setPinned(b)` | Held in place and raised to the front | +| `isLocked()` / `setLocked(b)` | Read-only, undeletable, uncopyable | +| `getZIndex()` / `setZIndex(n)` | Stacking order; higher is nearer front | +| `getMeta()` | `{author, createdAt, updatedAt}` | +| `saveNoteState()` | A plain copy of all of the above | + +Everything else — `getText`, `setText`, `moveTo`, `setSize`, `setCollapsed`, +`dispose` — is Blockly's, and documented there. + +Two things about `setLocked` are worth knowing before you build on it. + +**Locking owns `editable` and `deletable` outright.** Unlocking sets both back +to `true`, so if your app had independently made a note read-only and you then +lock and unlock it, that read-only state is gone. Drive one or the other, not +both. Pinning owns `movable` the same way, which is why the two compose +cleanly. + +**`canToggleLock` gates the menu, not the model.** `setLocked` always works +from code — undo, paste and loading a file all go through it. Locking states +intent and stops accidents; it is not a security boundary. + +## Helpers + +| Function | What it does | +| ------------------------- | ------------------------------------------------- | +| `isNote(x)` | Whether a workspace comment is one of ours | +| `restackNotes(workspace)` | Reorders notes on screen to match their z-indices | +| `saveNote(note, opts?)` | One note to JSON | +| `appendNote(state, ws)` | JSON back to a note on the workspace | +| `migrate(payload)` | Upgrades an older payload to the current version | +| `noteToDom(note)` | One note to a `` element | +| `domToNoteState(elem)` | A `` element to JSON | +| `domToNote(elem, ws)` | A `` element to a note on the workspace | + +```js +import {isNote, saveNote} from '@mit-app-inventor/blockly-workspace-notes'; + +const notes = workspace.getTopComments(false).filter(isNote); +const json = notes.map((note) => saveNote(note, {addCoordinates: true})); +``` + +## Types + +`SavedNote`, `NotesPayload`, `NoteState`, `NoteMeta`, `PaletteEntry`, +`WorkspaceNotesOptions`, `NoteCopyData`, `NoteChangeJson`, `NoteProperty`, +`NotePropertyValue` and `NoteSurface` are all exported for TypeScript users. + +## For extending the plugin + +`NoteSerializer`, `LegacyCommentAdapter`, `NotePaster` and `NoteChange` are +exported so you can subclass or inspect them. `NOTE_SERIALIZER_NAME`, +`SCHEMA_VERSION`, `DEFAULT_COLOUR` and `DEFAULT_PALETTE` are the constants +worth reading. diff --git a/blockly-workspace-notes/docs/design.md b/blockly-workspace-notes/docs/design.md new file mode 100644 index 0000000..e9f11f8 --- /dev/null +++ b/blockly-workspace-notes/docs/design.md @@ -0,0 +1,199 @@ +# Design + +A Blockly workspace is full of blocks, and blocks only say what the program +does. They cannot say why. Notes are somewhere to put the human part: a +reminder, a question for a teammate, a warning about a tricky bit. + +The requirement everything else follows from: **a note must survive a save and +reload.** A note that disappears when you close the tab is worse than useless, +because people will trust it and then lose work. + +## What is new, and what comes for free + +Blockly already has workspace comments — a plain box you can type into. They +already handle a great deal: + +| Already in Blockly | New here | +| -------------------------------------------- | ------------------------ | +| A box you can type into | A title | +| Drag to move, drag a corner to resize | A colour per note | +| Collapse, delete, copy and paste | Pinning — hold in place | +| Keyboard navigation and screen-reader labels | Locking — make read-only | +| Undo and redo | Stacking order | +| | Author and dates | +| | A versioned save format | + +Everything in the left column keeps working exactly as people already expect. + +## The decisions worth explaining + +### Why extend Blockly's comment rather than build a new object + +Dragging, resizing, keyboard access, screen readers, copy and paste, and undo +all already work. Rebuilding them would mean re-creating bugs that are already +fixed. + +The cost is that the design is tied to how Blockly's comments behave. If +Blockly changes them, this has to follow. + +### Why the note looks like a Blockly comment + +A note is a Blockly workspace comment, so it looks like one: square, thin +bordered, with a title bar in its own colour and its controls in that bar. + +An earlier design went the other way — a rounded card with no bar and no +buttons, everything in the right-click menu — on the theory that a rectangle +with a strip of icons across the top is what a block looks like. It read well, +but it made a note something you had to learn. Following the pattern people +already know is worth more than avoiding a family resemblance, and the colour +does the work of telling a note from a block. + +Collapse and delete are Blockly's own buttons, not copies. They are hidden by +default and this simply shows them, which is why they arrive with keyboard +navigation, focus handling, ARIA labels and the collapse button's automatic +relabelling between "Collapse Comment" and "Expand Comment" already working. All +that is replaced is the artwork, redrawn in the note's own ink. + +The pin beside them is a marker rather than a control: it does nothing when +clicked, and it is there only while a note is pinned, answering the one question +the note could not otherwise answer — why will this one not move? + +### Why the title is edited in place + +An earlier version asked for the title in a dialog. That was wrong twice over: +Blockly never asks for text in a dialog, and the fallback is the browser's own +serif prompt box, which matches nothing else on the page. + +Using Blockly's field editor was only half the fix, because a field editor is +built to be _seen_ — a white box with the text selected — and on a note that +still read as a mode opening on the note. The editor is now invisible: same +position, same font, same placeholder, no box, no selection. The title should +behave exactly like the body, and the body has never needed anything to open. + +The cost is that a click on the title does two things depending on whether it +moved. Blockly's own gesture code draws that line at the drag radius, and this +follows it, so dragging a note by its title still works. + +### Why "pinned" means held to the canvas, not fixed to the screen + +Pinned could mean held to the canvas, or fixed to the screen like a heads-up +display. Held won: a note that floats over your blocks wherever you pan is more +annoying than helpful, and the screen-fixed version fights the way a workspace +scrolls and zooms. + +### Why locking is not pinning + +They sound alike and they are not. Pinning is about _where a note is_ — it stops +being dragged and comes to the front. Locking is about _what a note says_ — it +stops being edited and cannot be deleted. + +Underneath, Blockly gives a comment three independent flags, and each of the two +features owns a disjoint set: locking drives `editable` and `deletable`, pinning +drives `movable`, and neither ever writes the other's. That is not tidiness for +its own sake. If both wrote `movable`, unpinning a locked note would quietly +hand its movement back, and which behaviour you got would depend on the order +you happened to do things in. Keeping them disjoint is what makes all four +combinations mean exactly what they look like. + +The cost accepted in exchange: unlocking sets both of its flags to `true` +outright, so locking owns them. A host wanting its own read-only notion should +not also drive those two. + +The title bar has one marker slot, and a lock takes it from a pin. A note that +cannot be edited is the more urgent of the two facts to convey, and the pin +comes back the moment it is unlocked. The alternative — showing both — costs the +title a fifth of its width on the notes most likely to have something to say. + +### Why the host decides who can unlock + +Locking exists because someone wants a note to survive being read by someone +else: a teacher's instructions on a student's workspace. That only works if the +student cannot simply unlock it, and the plugin has no idea who is looking. So +the host supplies a predicate and the plugin asks it. + +It is held per workspace rather than captured once, because every other +registration here is first-registration-wins — harmless for a palette, wrong +for a permission, since a host running two workspaces would otherwise let +whichever loaded first decide for both. + +And it gates the menu, not the model. `setLocked` has to keep working from code +or undo, paste and loading a file would all break. Locking states intent and +stops accidents; it is not a security boundary, and anything that depends on it +should be checked where the file is saved. + +### Why every colour comes from one + +A note stores a single colour. Everything else is derived from it: the title bar +and border a step darker, and the ink — the title, the body text and all three +glyphs — the same hue taken dark and saturated. + +Nothing is hardcoded black, and that is the point. Black furniture on a coloured +card looks like a coloured card with black furniture on it; a green note whose +text and icons are deep green reads as one object. It also means a host can +supply any palette it likes and the whole note follows. + +The one thing derivation cannot guarantee is readability, so that is tested +rather than trusted: across the palette the worst contrast is 5.79:1 for ink on +the bar and 7.67:1 on the body, both clear of the 4.5:1 that body text needs, +and `test/colour.mocha.js` asserts it. + +The fills themselves stay pale because notes sit among coloured blocks, and a +saturated note would compete with them: S 0.25 at V 0.98, against a block's +S 0.45 at V 0.65. + +### Why notes get their own place in the save file + +Notes are saved under their own key rather than pretending to be plain +comments. That keeps the extra fields clean and versioned, and means a note is +never saved twice by accident. + +The cost is that a file with notes in it is not a plain Blockly file — though a +plain Blockly editor ignores them rather than breaking. + +## How the code is laid out + +Each folder is one layer, and a file is named for the single thing it holds. +Reading top to bottom is roughly the dependency order. + +``` +src/ +├── index.ts The whole public surface. Re-exports only, no logic. +├── plugin.ts WorkspaceNotes: what a host constructs. +├── model/ What a note is. +├── serialization/ JSON in and out, plus xml/ for the older format. +├── events/ The undo event a note fires. +├── clipboard/ Pasting a note with its title and colour intact. +├── ui/ Chrome the user touches: menu, title editor, CSS. +├── utils/ Pure helpers. No registration, no module state. +├── constants/ Numbers and names, grouped by what they configure. +└── types/ The shapes that travel between the layers. +``` + +Four rules keep it that way: + +- **`index.ts` holds no logic.** Anything it does not re-export is internal and + free to move. It is also the build entry point, resolved by path, so it stays + where it is. +- **No barrel files inside the folders.** Every import names the file it wants + (`../constants/layout`, never `../constants`). Longer to type, and the reason + the import graph stays legible as the code grows. +- **`utils/` is inert.** Pure functions, no Blockly registration, no + module-level state — importable from a test with nothing set up. +- **Registration lives in `registry.ts`.** Anything that mutates a global + Blockly registry does it in a file named `registry.ts` (or `xml/patch.ts`), + so the side effects are findable in one sweep and every one has a matching + `unregister`. + +The graph is acyclic, and a test says so. `test/imports.mocha.js` walks every +file and fails on any loop, because a cycle here would not fail honestly: the +production bundle flattens modules into one scope, where a cycle becomes an +ordering problem, while the test bundle keeps them apart and papers over it. A +cycle that broke would break only in `dist/`. + +The thing that keeps it acyclic is small and easy to undo by accident. A note +restacks its neighbours when its z-index changes, so `model/note.ts` imports +`model/stacking.ts`; for the graph to stay linear, nothing under that may +import a note class back. So `restackNotes` recognises a note by a mark the +mixin puts on it (`constants/brand.ts`) rather than by `instanceof`, and raises +it through a `bringToFront` seam rather than by reaching for the rendered +class. Both of those exist for this reason and no other. diff --git a/blockly-workspace-notes/docs/getting-started.md b/blockly-workspace-notes/docs/getting-started.md new file mode 100644 index 0000000..f5c486a --- /dev/null +++ b/blockly-workspace-notes/docs/getting-started.md @@ -0,0 +1,94 @@ +# Getting started + +## Install + +```bash +npm install @mit-app-inventor/blockly-workspace-notes +``` + +You need Blockly 13.2.1 or newer. It is a peer dependency, so it stays on your +side of the install. + +## Switch it on + +```js +import * as Blockly from 'blockly'; +import {WorkspaceNotes} from '@mit-app-inventor/blockly-workspace-notes'; + +const workspace = Blockly.inject('blocklyDiv', {toolbox}); +new WorkspaceNotes(workspace).init(); +``` + +That is the whole setup. Right-click the workspace and you will see **Add +Note**. + +> **Import the plugin before `Blockly.inject`.** +> Blockly's stylesheets only reach workspaces injected _after_ they are +> registered, so a note imported later comes out unstyled. + +## Options + +Pass an object as the second argument. Every option has a sensible default, so +pass only what you want to change. + +```js +new WorkspaceNotes(workspace, { + defaultSize: {width: 300, height: 200}, + getAuthor: () => currentUser.name, +}).init(); +``` + +| Option | Default | What it does | +| ---------------------------- | -------------------- | ----------------------------------------------- | +| `palette` | 7 stationery colours | The swatches offered in the Colour menu | +| `defaultSize` | `260 × 180` | The size a new note is created at | +| `getAuthor` | `() => ''` | Names the note's author, shown along its foot | +| `canToggleLock` | `() => true` | Decides who may lock and unlock a note | +| `contextMenu` | `true` | Adds the note items to the right-click menu | +| `xmlSupport` | `true` | Keeps notes intact through the older XML format | +| `skipSerializerRegistration` | `false` | Leaves saving and loading entirely to your app | +| `emitLegacyComments` | `false` | Also writes the old `workspaceComments` key | + +### A custom palette + +Each entry needs a name, a hue and the resolved fill colour: + +```js +new WorkspaceNotes(workspace, { + palette: [ + {name: 'Yellow', hue: 48, fill: '#f9edbb'}, + {name: 'Sky', hue: 200, fill: '#bbe5f9'}, + ], +}).init(); +``` + +### Who may unlock a note + +A locked note is read-only and cannot be deleted. By default anyone can lock +and unlock one; pass `canToggleLock` when that is somebody's decision to make: + +```js +new WorkspaceNotes(workspace, { + canToggleLock: () => currentUser.isTeacher, +}).init(); +``` + +The predicate is given the note, so it can answer differently for different +ones. It gates the menu, not the model — `note.setLocked(false)` still works +from code, because undo, paste and loading a file all depend on it. If the +guarantee matters, enforce it where you save. + +## Turning it off + +`dispose()` puts Blockly back exactly as it found it — the menu items, the +clipboard, the save format and the default comment size all return to normal. + +```js +const notes = new WorkspaceNotes(workspace); +notes.init(); + +// later +notes.dispose(); +``` + +Both calls are safe to repeat; a second `init()` or `dispose()` does nothing. diff --git a/blockly-workspace-notes/docs/images/note-anatomy.svg b/blockly-workspace-notes/docs/images/note-anatomy.svg new file mode 100644 index 0000000..f76a181 --- /dev/null +++ b/blockly-workspace-notes/docs/images/note-anatomy.svg @@ -0,0 +1,105 @@ + + The parts of a note, and what locking changes + Two notes at the default size of 260 by 180. The first labels the collapse button, the title, the delete button, the body, the author and date line, and the resize handle. The second is locked: it shows a padlock in the marker slot, and has no delete button and no resize handle. + + + + The parts of a note + + + + + + + Shopping list + milk + bread + coffee + + ada + + 9 Sep 2026 + + + + + + + + + + + + + + Title + click it to rename, in place + + Collapse + folds the note to its bar + + Author and date + recorded automatically + + Delete + one undo brings it back + + The body + written in the note's own ink + + Resize handle + inside the sheet, not on its corner + + + + + + + + the title bar, one step darker than the body + + What locking changes + + + + + + + Read the brief + Written by your teacher. + + ada + + 9 Sep 2026 + + + + + + + + + + + Marker slot + a padlock here, or a pin when pinned + + The body + read-only, and the title with it + + No delete button + and no Delete in the menu + + No resize handle + the note keeps the size it was left + + diff --git a/blockly-workspace-notes/docs/images/note-states.svg b/blockly-workspace-notes/docs/images/note-states.svg new file mode 100644 index 0000000..260ee55 --- /dev/null +++ b/blockly-workspace-notes/docs/images/note-states.svg @@ -0,0 +1,123 @@ + + The states a note can be in + Six notes: a named note, one not named yet, a collapsed one, a pinned one showing its marker, a selected one, and a locked one showing a padlock and no delete button. + + + + Named + the title is what you scan for + + + + + + Shopping list + milk + bread + + ada + + 9 Sep 2026 + + + + Not named yet + faded placeholders, both rows + + + + + + Title + Say something... + + 9 Sep 2026 + + + + Collapsed + the bar is the whole note + + + + + + Release checklist + + + Pinned + a pin in the bar, and a heavier edge + + + + + + + Do not move + Held in place. + + ada + + 9 Sep 2026 + + + + Selected + the ring follows the card + + + + + + Ideas + Try a smaller step. + + ada + + 9 Sep 2026 + + + + + Locked + a padlock in the bar, and no bin + + + + + + Read the brief + Written by your teacher. + + ada + + 9 Sep 2026 + + + diff --git a/blockly-workspace-notes/docs/saving.md b/blockly-workspace-notes/docs/saving.md new file mode 100644 index 0000000..7da00fc --- /dev/null +++ b/blockly-workspace-notes/docs/saving.md @@ -0,0 +1,104 @@ +# Saving and loading + +Notes save with the workspace. There is nothing extra to call. + +```js +const state = Blockly.serialization.workspaces.save(workspace); +Blockly.serialization.workspaces.load(state, workspace); +``` + +## What a saved note looks like + +Notes get their own key in the file, beside `blocks`: + +```json +{ + "blocks": {"languageVersion": 0, "blocks": []}, + "workspaceNotes": { + "version": 1, + "notes": [ + { + "id": "n1qX", + "x": 40, + "y": 20, + "width": 240, + "height": 140, + "text": "Refactor this loop", + "title": "TODO", + "colour": "#f9bbc5", + "pinned": true, + "locked": true, + "zIndex": 3, + "meta": { + "author": "ada", + "createdAt": "2026-09-07T16:07:09Z", + "updatedAt": "2026-09-07T16:09:41Z" + } + } + ] + } +} +``` + +Everything that gets remembered: + +| Saved | Notes | +| --------------------- | -------------------------------------------------------------------------------------------------------- | +| Position | Exact, including right-to-left layouts | +| Width and height | Exactly as you left them | +| The text | | +| The title | Only if it has one | +| The colour | Only if it is not the default | +| Pinned | Only if pinned | +| Locked | Only if locked; it implies neither editable nor deletable, so neither is written | +| Stacking order | So overlapping notes come back in the same order | +| Collapsed | Only if collapsed | +| Author and timestamps | Author comes from `getAuthor`; dates are automatic, and `updatedAt` moves on any edit including the body | + +## Three things about the format + +**Only what differs is written.** A plain yellow note with no title records +neither. Files stay small, and a change to one note shows up as a small diff +rather than a wall of text. + +**The payload carries a version.** It moves when a file written by an older +release cannot simply be read by a newer one — a field renamed, retyped or +moved — and such files are then upgraded quietly as they load. Adding an +optional field is not that: it is absent from older files, which is exactly +what its default means, so the version stays where it is. + +**Old files still open.** A workspace saved before this plugin existed, with +plain Blockly comments in it, loads correctly — each comment becomes a note +with the default colour and no title. + +## XML + +Blockly's older XML format works too, as long as `xmlSupport` is on, which it +is by default. + +```js +const dom = Blockly.Xml.workspaceToDom(workspace); +Blockly.Xml.domToWorkspace(dom, workspace); +``` + +A note is written as a `` element with a few extra attributes: + +```xml +Refactor this loop +``` + +Every added attribute is optional, so plain Blockly still reads the element as +an ordinary comment and ignores what it does not recognise. + +JSON is the format to prefer. Blockly has frozen XML — it still works and is +not going away, but it gains no new features, and only JSON has a proper place +for a plugin's own data. + +## Saving it yourself + +If your app handles persistence its own way, pass +`skipSerializerRegistration: true` and use `saveNote` and `appendNote` +directly. See the [API](./api.md). diff --git a/blockly-workspace-notes/docs/using-notes.md b/blockly-workspace-notes/docs/using-notes.md new file mode 100644 index 0000000..bf8661d --- /dev/null +++ b/blockly-workspace-notes/docs/using-notes.md @@ -0,0 +1,128 @@ +# Using notes + +A note is a card on the workspace: a title bar you can read at a glance, a +colour of its own, and room to write underneath. + +

+ The parts of a note: title bar, collapse and delete buttons, title, body, author and date, and resize handle; and below it a locked note with a padlock, no delete button and no resize handle +

+ +The title bar carries two buttons — collapse on the left, delete on the right — +and one marker slot between them. A pin appears there on a pinned note, a +padlock on a locked one, and the padlock wins if a note is both. A locked note +also loses the bin from its bar. Along the foot, a small line records who wrote +the note and when it last changed. Everything else a note can do is in the +right-click menu. + +## Add one + +Right-click empty canvas and choose **Add Note**. It appears where you clicked, +ready to type in. + +## Write in it + +Click the body and type. An empty note shows a faded prompt so it never looks +broken. + +## Name it + +Click the title and type. Nothing opens and nothing moves — the heading just +gains a caret, the same way the body does. + +- **Enter** commits. +- **Escape** puts the old title back. +- Clicking elsewhere commits. + +Dragging a note by its title still moves it. The editor only opens if the press +stayed put. + +## Colour it + +Right-click and pick from a row of seven colours. The current one is ringed. + +The swatches sit in a single row rather than seven separate menu entries, so +picking a colour is one click and the menu stays short. + +## Pin it + +Right-click → **Pin note**. A pinned note cannot be dragged and sits in front +of its neighbours. It says so twice: a pin in the title bar, and a heavier edge +around the card. + +The pin is a marker, not a button — there is nothing to click, and it is only +there while the note is pinned. Right-click → **Unpin note** to release it. + +## Lock it + +Right-click → **Lock note** to make a note read-only. A locked note shows a +padlock in its title bar and loses the bin beside it. + +Locked, you cannot edit the text, rename it, recolour it, resize it, delete it +or copy it. You can still move it, collapse it, restack it, select it and read +it. Right-click → **Unlock note** to release it. + +Whether you may lock or unlock a note at all is up to the app — a teacher can +be allowed to lock an instruction note that a student cannot unlock. Where that +is the case, **Unlock note** is still in the menu but greyed out, so it is +clear the note is locked deliberately rather than broken. + +## Collapse it + +Press the chevron at the left of the title bar to fold a note down to that bar, +or right-click → **Collapse note**. The title stays readable, and the chevron +turns to point right. Press it again to open the note back up. + +This is how you keep a long note around without it covering your blocks. + +## Who wrote it, and when + +Along the bottom of every note is a quiet line showing its author and the date +it last changed. Both are recorded automatically — the date every time you +edit, and the author from the `getAuthor` option your app supplies. Without +that option there is no name to show, so the line falls back to the date alone. + +On a narrow note the name gives way first, since the date is short and the name +can be cut to nothing useful. A collapsed note hides the line with its body. + +## Order them + +Right-click → **Bring to front** or **Send to back**, for when notes overlap. +The order is remembered when you save. + +## Move, resize, copy, delete + +These are Blockly's own, and they behave exactly as they do for anything else +on the workspace. + +- Drag the title row to move it. +- Drag the bottom-right corner to resize. The title shortens with an ellipsis + as you go, and the note stops at a size where it can still be read and + grabbed. A locked note has no resize handle. +- **Duplicate note**, or copy and paste. The copy keeps the title, colour and + pinned state, and lands slightly offset. A locked note cannot be copied. +- **Delete note** from the menu, the bin at the right of the title bar, or + Delete while the note is selected. None of the three works on a locked note. + +One press of undo brings a deleted note back complete — same text, same title, +same colour, same pinned state. Every other change is undoable too: typing, +resizing, recolouring, renaming, pinning, locking, reordering. + +## The states you will see + +

+ A named note, one not named yet, a collapsed note, a pinned note showing its marker, a selected note, and a locked note showing a padlock and no bin +

+ +| State | How you can tell | +| ----------------- | ----------------------------------------------------- | +| **Named** | The title is bold, in the note's own ink | +| **Not named yet** | A faded `Title`, and a faded prompt in the body | +| **Collapsed** | Just the title bar, and the chevron points right | +| **Pinned** | A pin in the title bar, and a heavier edge | +| **Locked** | A padlock in the title bar, and no bin beside it | +| **Selected** | Blockly's own gold ring, following the card's corners | + +## Keyboard and screen readers + +Notes are workspace comments underneath, so Blockly's keyboard navigation and +screen-reader support apply to them unchanged. Nothing extra to configure. diff --git a/blockly-workspace-notes/eslint.config.mjs b/blockly-workspace-notes/eslint.config.mjs new file mode 100644 index 0000000..bcb51d5 --- /dev/null +++ b/blockly-workspace-notes/eslint.config.mjs @@ -0,0 +1,116 @@ +/** + * @fileoverview ESLint configuration. + * + * Blockly publishes `@blockly/eslint-config`, but it still peers on ESLint 7 + * and pulls in the deprecated `babel-eslint`, so this reproduces the house + * style it encodes — Google-ish source with mandatory documentation — on a + * current toolchain instead. + * + * The source is TypeScript and the tests are JavaScript, which is not a + * stylistic choice: `@blockly/dev-scripts` finds test entry points with a + * literal `.mocha.js` filename filter, so a `.mocha.ts` suite is silently + * skipped and `npm test` passes having run nothing. The two `files` blocks + * below reflect that split. + * + * Formatting rules are left entirely to Prettier: `eslint-config-prettier` + * goes last and switches off everything the two would otherwise argue about. + */ + +import js from '@eslint/js'; +import jsdoc from 'eslint-plugin-jsdoc'; +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default [ + { + ignores: ['dist/**', 'build/**', 'node_modules/**'], + }, + + js.configs.recommended, + ...tseslint.configs.recommended, + jsdoc.configs['flat/recommended'], + + { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.es2021, + }, + }, + plugins: {jsdoc}, + settings: { + jsdoc: { + // Blockly writes @fileoverview in every core file; the plugin's + // default is to rewrite it to @file. Invert that preference. + tagNamePreference: {file: 'fileoverview'}, + mode: 'typescript', + }, + }, + rules: { + // Correctness. + 'no-var': 'error', + 'prefer-const': 'error', + eqeqeq: ['error', 'always', {null: 'ignore'}], + 'no-throw-literal': 'error', + 'no-implicit-coercion': ['error', {boolean: false}], + + // The base rule double-reports on type-only imports and on enum-like + // declarations, so the TypeScript-aware version replaces it. + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}], + + // Blockly documents every exported symbol, and so does this package — + // but the types now live in the signatures, so the doc comment carries + // the prose and nothing else. + 'jsdoc/require-jsdoc': [ + 'error', + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + }, + }, + ], + 'jsdoc/require-param': 'error', + 'jsdoc/require-param-description': 'error', + 'jsdoc/require-returns': 'error', + 'jsdoc/require-returns-description': 'error', + + // Types belong to the compiler now. `no-types` is what keeps the old + // Closure annotations from creeping back in beside the real ones. + 'jsdoc/no-types': 'error', + 'jsdoc/require-param-type': 'off', + 'jsdoc/require-returns-type': 'off', + + 'jsdoc/require-description-complete-sentence': 'off', + 'jsdoc/tag-lines': 'off', + }, + }, + + { + // The playground and the mocha suites are development-only entry points, + // and are JavaScript for the toolchain reason described above. + files: ['test/**/*.js'], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + ...globals.mocha, + }, + }, + rules: { + // Test callbacks are self-describing; requiring JSDoc on every suite + // and case would be noise rather than documentation. + 'jsdoc/require-jsdoc': 'off', + // The suites are plain JavaScript, so their doc comments still carry + // types where they help. + 'jsdoc/no-types': 'off', + }, + }, + + prettier, +]; diff --git a/blockly-workspace-notes/package-lock.json b/blockly-workspace-notes/package-lock.json new file mode 100644 index 0000000..59dfcad --- /dev/null +++ b/blockly-workspace-notes/package-lock.json @@ -0,0 +1,8514 @@ +{ + "name": "@mit-app-inventor/blockly-workspace-notes", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@mit-app-inventor/blockly-workspace-notes", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "@blockly/dev-scripts": "^13.2.0", + "@blockly/dev-tools": "^13.2.0", + "@eslint/js": "^10.0.1", + "blockly": "^13.2.1", + "eslint": "^10.10.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsdoc": "^64.3.6", + "globals": "^17.12.0", + "prettier": "^3.9.6", + "typescript": "5.9.3", + "typescript-eslint": "^8.70.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "blockly": "^13.2.1" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@babel/code-frame": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz", + "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^8.0.0", + "js-tokens": "^10.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", + "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@blockly/block-test": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-13.2.0.tgz", + "integrity": "sha512-wOZYCt8HE8Q8FxpQhOJwxXT4m0odNKvSbUuGKHvoJU7Cba+sM04EZaXuOU0MNb4B84fCE7Z4c17vM9nHH1Us7g==", + "dev": true, + "license": "Apache 2.0", + "peerDependencies": { + "blockly": "^13.2.0" + } + }, + "node_modules/@blockly/dev-scripts": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@blockly/dev-scripts/-/dev-scripts-13.2.0.tgz", + "integrity": "sha512-fI/X3ndKFOeJRAnD57GyIjNI347zCGy/3wyRq1MRrio0k66Bz9ILzF3thNpfBg70ZOhh6iDTl7E/+uguKVDvDw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "chalk": "^4.1.2", + "fork-ts-checker-webpack-plugin": "^9.1.0", + "install": "^0.13.0", + "mocha": "^11.7.6", + "rimraf": "^6.1.3", + "source-map-loader": "^5.0.0", + "ts-loader": "^9.6.1", + "webpack": "^5.107.2", + "webpack-cli": "^7.0.3", + "webpack-dev-server": "^5.2.5" + }, + "bin": { + "blockly-scripts": "bin/blockly-scripts.js" + }, + "peerDependencies": { + "typescript": "^4.3.2 || ^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@blockly/dev-tools": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-13.2.0.tgz", + "integrity": "sha512-D0OdJwTsLV8h+lod7g1HNmfsHYmy+2TibKxWf0ywRsBDiObYa/QZ8UaqPPvN6Ixeq7pFEvoYQEH2TEHpG+p4yQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@blockly/block-test": "^13.2.0", + "@blockly/theme-dark": "^13.2.0", + "@blockly/theme-deuteranopia": "^13.2.0", + "@blockly/theme-highcontrast": "^13.2.0", + "@blockly/theme-tritanopia": "^13.2.0", + "chai": "^6.2.2", + "dat.gui": "^0.7.9", + "lodash.assign": "^4.2.0", + "lodash.merge": "^4.6.2", + "monaco-editor": "^0.55.1", + "sinon": "^22.0.0" + }, + "peerDependencies": { + "blockly": "^13.2.0" + } + }, + "node_modules/@blockly/theme-dark": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-13.2.0.tgz", + "integrity": "sha512-HSydDiVn7W8RIRTNZNtcvbrDbqx+8IjVOizGDy12AncZ65PfiTV7OsZxqeyn4J+oewNVhF5n3GtufxK/ca7iZA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "blockly": "^13.2.0" + } + }, + "node_modules/@blockly/theme-deuteranopia": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-13.2.0.tgz", + "integrity": "sha512-on0Z/MpGQSUxacNXiaqpr7WCxeSXc1aP+XjN1+8zo8nm18JUY94WywXNpyiYD7Rbxxzx9uBX2OEcVXCwWPlMyQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "blockly": "^13.2.0" + } + }, + "node_modules/@blockly/theme-highcontrast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-13.2.0.tgz", + "integrity": "sha512-87dUL4zckRSBDxLOLbwEvvF/ghARouKw5IM62rCCrOZUesZBrAJh8zuEhzdHEN7X6SybJWUmSTq+hR5EmHsUPw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "blockly": "^13.2.0" + } + }, + "node_modules/@blockly/theme-tritanopia": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-13.2.0.tgz", + "integrity": "sha512-C7uaW57B3HOC/aZTvXYSWwXXAruftUw7nThbdyLaK2SDHuy0L2V8voxZywbxI/Dvda797XhZEt1SRigbFf6tww==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "blockly": "^13.2.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz", + "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peer": true, + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", + "integrity": "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.97.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.97.0.tgz", + "integrity": "sha512-EP8uoFfh6+GsdGCduYtmWAW0h7AO+Ayik9Vh5YbA2r/3N6lmJKkCNZX+q3QBXC1K6ixjQ/9igF2b7WVvLm063g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.9", + "@typescript-eslint/types": "^8.69.0", + "comment-parser": "1.4.8", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~9.2.1" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + } + }, + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.71.0.tgz", + "integrity": "sha512-9DFR/j+bm+tig1abs1CWS1/r0IVjNUdTp/+q6R/PXayXpO1rgbUh7tkanGYP40I0zdxbOreN3tmzBpVHgfMKzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.71.0.tgz", + "integrity": "sha512-dRw5ojxzep3lntVGVBLzQUMYj1hXLH+DAz9sK0vKU+594x/zA2nYPOiitlyhYtOJ2nv1FXNq7c55rgwANknIWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.71.0", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.71.0.tgz", + "integrity": "sha512-yt5Ak0otPdHPIlmMvF16aLG2qSZL52EYJ7HOkYpBcIPWw+kmT1FtTSj4oiV2OUkJbG8pLzA+RhMgrlRl85QuBw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.71.0", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", + "@jsonjoy.com/fs-print": "4.71.0", + "@jsonjoy.com/fs-snapshot": "4.71.0", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.71.0.tgz", + "integrity": "sha512-BSzl+QFSxZF58BxGjV1wqCJ+qSn3b2IZnb+z3hDQq6goqOO5RuxF8gRihjsFx17AfpEpa7XjYcP8XpQtDU8RrQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.71.0.tgz", + "integrity": "sha512-OlXBZKIeDx5bIGcQmn0w+nVkLheCiuQSepKiBAiWSWimSdn8Q6Yc9ih2y8zStcJk31fieqnov9V+o8XTWn/5Rw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.71.0", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.71.0.tgz", + "integrity": "sha512-YtzCL3jbKYx6LHxqS9ymJ9Ob7SO9cucI+kpNO3LijGNFEjFbvNsTlHkvFgocAMAjUk96TpQTCsYkzFQi1CdlAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.71.0.tgz", + "integrity": "sha512-OhfDSdvyO8uGV0U11OP+mOeVCPgjuP1HqCGR/TdBQLxHoDEWJAK3KOP5fPXo57H7i3G0x0Vmv8xPRHDle4XGzA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.71.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.71.0.tgz", + "integrity": "sha512-md+Wov365xa9A3nfW9YQTHLcweHHAee/e/0UoVRbldEHxboPJknuO3sEkNVVECO0dTqKra/ERaQylAMRrGWd0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz", + "integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz", + "integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz", + "integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz", + "integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz", + "integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz", + "integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pfx": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz", + "integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", + "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz", + "integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz", + "integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz", + "integrity": "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/are-docs-informative": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.1.1.tgz", + "integrity": "sha512-sqRsNQBwbKLRX0jV5Cu5uzmtflf892n4Vukz7T659ebL4pz3mpOqCMU7lxMoBTFwnp10E3YB5ZcyHM41W5bcDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blockly": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/blockly/-/blockly-13.2.1.tgz", + "integrity": "sha512-iOa37H0ks5Zup2G1zZyviLX9E5WzWh3h0Zs8Nta2cPKmnAGHjZ6wrp3VqLTaquTJReJKPwaJZ39K7AD24rT4lg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "jsdom": ">=27.4.0 <30.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.4.tgz", + "integrity": "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/comment-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.8.tgz", + "integrity": "sha512-rKZTGo4fzKYna8UcL0isTg5wkBNla7bxTypLwZQXjIdi++IdP1OJ41rI5Mti3/jltkPujbu4i9LIARYA+zpotQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.422", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", + "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "64.3.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-64.3.6.tgz", + "integrity": "sha512-lo7IXmgUUNy88SxW7KnJmmD2iPQBIRMomfCFPHidW1M3zNNVuzxlB2uoNrNyE1g4v2/L+RtYmiROPwQhSNzL8Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.97.0", + "@es-joy/resolve.exports": "1.2.0", + "@typescript-eslint/utils": "^8.69.0", + "are-docs-informative": "^0.1.1", + "comment-parser": "1.4.8", + "debug": "^4.4.3", + "escape-string-regexp": "^5.0.0", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.1", + "parse-imports-exports": "^0.2.4", + "semver": "^7.8.5", + "spdx-expression-parse": "^5.0.0", + "to-valid-identifier": "^1.0.0" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-9.2.1.tgz", + "integrity": "sha512-V4Ww4EHnTcTLSOMoB0FsF72JhQvcAsriCm/LWnxJeGWoxIjEL2l9na11abQok5SYShq8m0Gl02el/xAbTCulvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.9", + "@types/node": "^26.4.0" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + } + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0", + "peer": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.10.0.tgz", + "integrity": "sha512-2//60T4S0X1Ug/dqLDOgDaGlZsN1Lv8hf8oB0NOV/KIHSaXlPwXBBIbim79dE2Z7NEs52ES8YHoSv6Lmsk+XNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@napi-rs/image": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "imagemin": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "sharp": { + "optional": true + }, + "svgo": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", + "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-deep-merge": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.1.tgz", + "integrity": "sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/parse-json/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/parse-json/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sinon": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.1.0.tgz", + "integrity": "sha512-n1ajF2rBWMTtEwbKcw4UdFg4nCnDdq/U6RDoxtOd7oapOlRoJ5ynwFx60owROyhDpA9QhMZi0pCO/xtmwFjG7w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^15.4.0", + "@sinonjs/samsam": "^10.0.2", + "diff": "^9.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-5.0.0.tgz", + "integrity": "sha512-vngmw3Rgn+o2arXNbnZaj5UtOEBuWBfvaI+Wc8GFfykIhA5/vdK9/Sp/XkLv63dykz2rxKDvKEHupF5P0FORcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thingies": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", + "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz", + "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tldts-core": "^7.4.12" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz", + "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-loader": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz", + "integrity": "sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "picomatch": "^4.0.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "loader-utils": "*", + "typescript": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack": { + "version": "5.110.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.3.tgz", + "integrity": "sha512-GuizBzRvo9YPpyoNMf3ag7AzxbaW85qrRSqTha345KyJbAFPt3/cMzBM0h+RWg7SK/7DdzRLINP3LvQ0hvr4hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.2.3.tgz", + "integrity": "sha512-vDFU7jrfCctnN7jJQWPl+V26B51GLp11prVZXg50oeonsgeBzSTJEWmXkjHsOjTgMjlPOQM8GWLh33X9RN/0ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^1.1.0", + "commander": "^14.0.3", + "cross-spawn": "^7.0.6", + "envinfo": "^7.21.0", + "import-local": "^3.2.0", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "js-yaml": "^4.0.0 || ^5.0.0", + "json5": "^2.2.3", + "toml": "^3.0.0 || ^4.0.0 || ^5.0.0", + "webpack": "^5.101.0", + "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", + "webpack-dev-server": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "js-yaml": { + "optional": true + }, + "json5": { + "optional": true + }, + "toml": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.6.tgz", + "integrity": "sha512-yBWCMvIfUmuhAE8vdqUKzH0vg9kuWN0KeG4vBnqRplUFHRU7lMQjkiJWxVQzvo2BTewqhPhDlMB41rAt2jVA9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-middleware/node_modules/memfs": { + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.71.0.tgz", + "integrity": "sha512-Zwrk7TpTXBkic7taZL+2QeppbauG0QOufRsT1zuXDYLW8jCajH0W1VSZ17+I7ODqiNbH9J1Vf1QJCqFlCfurDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.71.0", + "@jsonjoy.com/fs-fsa": "4.71.0", + "@jsonjoy.com/fs-node": "4.71.0", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-to-fsa": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", + "@jsonjoy.com/fs-print": "4.71.0", + "@jsonjoy.com/fs-snapshot": "4.71.0", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.6.tgz", + "integrity": "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.8.1", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.22.1", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.14.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^5.5.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/blockly-workspace-notes/package.json b/blockly-workspace-notes/package.json new file mode 100644 index 0000000..58f7c2a --- /dev/null +++ b/blockly-workspace-notes/package.json @@ -0,0 +1,84 @@ +{ + "name": "@mit-app-inventor/blockly-workspace-notes", + "version": "0.1.0", + "description": "Sticky notes for a Blockly workspace: draggable, resizable, colour-coded comments with a title, that round-trip through JSON and XML.", + "scripts": { + "audit:fix": "blockly-scripts auditFix", + "build": "blockly-scripts build && npm run build:types", + "clean": "blockly-scripts clean", + "predeploy": "blockly-scripts predeploy", + "start": "blockly-scripts start", + "test": "blockly-scripts test", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "prepack": "npm run clean && npm run build", + "build:types": "tsc --emitDeclarationOnly", + "typecheck": "tsc --noEmit", + "pretest": "npm run build:types" + }, + "main": "./dist/index.js", + "unpkg": "./dist/index.js", + "author": "", + "keywords": [ + "blockly", + "blockly-plugin", + "workspace-comment", + "comment", + "notes", + "sticky-notes", + "annotation", + "serialization" + ], + "homepage": "https://github.com/mit-cml/blockly-plugins/tree/main/blockly-workspace-notes#readme", + "bugs": { + "url": "https://github.com/mit-cml/blockly-plugins/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mit-cml/blockly-plugins.git", + "directory": "blockly-workspace-notes" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "directories": { + "dist": "dist", + "src": "src" + }, + "files": [ + "dist", + "src", + "docs", + "README.md" + ], + "devDependencies": { + "@blockly/dev-scripts": "^13.2.0", + "@blockly/dev-tools": "^13.2.0", + "@eslint/js": "^10.0.1", + "blockly": "^13.2.1", + "eslint": "^10.10.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsdoc": "^64.3.6", + "globals": "^17.12.0", + "prettier": "^3.9.6", + "typescript": "5.9.3", + "typescript-eslint": "^8.70.0" + }, + "peerDependencies": { + "blockly": "^13.2.1" + }, + "publishConfig": { + "access": "public" + }, + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + } +} diff --git a/blockly-workspace-notes/src/clipboard/note_paster.ts b/blockly-workspace-notes/src/clipboard/note_paster.ts new file mode 100644 index 0000000..c33a4ac --- /dev/null +++ b/blockly-workspace-notes/src/clipboard/note_paster.ts @@ -0,0 +1,141 @@ +/** + * @fileoverview Clipboard support for notes. + * + * Core's paster rebuilds a comment through its own serializer, which knows + * nothing about titles or colours — so a copy/paste or duplicate would return + * a plain yellow note. This replaces it under the same paster type, since that + * is the type name `RenderedWorkspaceComment.toCopyData()` stamps onto the + * clipboard data. + */ + +import * as Blockly from 'blockly/core'; + +import {Note} from '../model/note'; +import {appendNote} from '../serialization/state'; +import type {NoteCopyData} from '../types/clipboard'; +import type {SavedNote} from '../types/serialization'; + +/** + * The paster type core registers for workspace comments. Matching it means + * both notes and any plain comments still route here. + */ +export const PASTER_TYPE = 'workspace-comment'; + +/** How far to nudge a pasted note that would land exactly on another. */ +const OFFSET_DISTANCE = 30; + +/** + * Pastes notes, preserving their note-specific state. + */ +export class NotePaster implements Blockly.IPaster { + /** + * @param copyData Data produced by `Note.toCopyData()`. + * @param workspace The workspace to paste into. + * @param [coordinate] Where to paste. + * @returns The pasted note, or null. + */ + paste( + copyData: NoteCopyData, + workspace: Blockly.WorkspaceSvg, + coordinate?: Blockly.utils.Coordinate, + ): Note | null { + const state = { + ...copyData.commentState, + ...(copyData.noteState ?? {}), + } as SavedNote; + + if (coordinate) { + state.x = coordinate.x; + state.y = coordinate.y; + } + + // Core builds the note silently and fires a single create event + // afterwards, so a paste is one undo step rather than a dozen. + Blockly.Events.disable(); + let note: Note | null; + try { + const created = appendNote(state, workspace); + // `workspace` is rendered, so `appendNote` took the rendered branch — + // and only a rendered note can be focused or nudged. + note = created instanceof Note ? created : null; + if (note) moveOutOfTheWay(note); + } finally { + Blockly.Events.enable(); + } + + if (!note) return null; + + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.CommentCreate(note)); + } + Blockly.getFocusManager().focusNode(note); + return note; + } +} + +/** + * Nudges a note diagonally until it no longer sits exactly on top of another. + * + * @param note The freshly pasted note. + */ +function moveOutOfTheWay(note: Note): void { + const workspace = note.workspace; + const coordinate = note.getRelativeToSurfaceXY(); + const offset = new Blockly.utils.Coordinate(0, 0); + const others = workspace + .getTopComments(false) + .filter((other) => other.id !== note.id) + .map((other) => other.getRelativeToSurfaceXY()); + + const overlaps = (candidate: Blockly.utils.Coordinate): boolean => + others.some( + (other) => + Math.abs(other.x - candidate.x) <= 1 && + Math.abs(other.y - candidate.y) <= 1, + ); + + while (overlaps(Blockly.utils.Coordinate.sum(coordinate, offset))) { + offset.translate( + workspace.RTL ? -OFFSET_DISTANCE : OFFSET_DISTANCE, + OFFSET_DISTANCE, + ); + } + + note.moveTo(Blockly.utils.Coordinate.sum(coordinate, offset)); +} + +/** Reference count, since the clipboard registry is a global singleton. */ +let registrationCount = 0; + +/** + * The paster displaced on first registration, kept so it can be put back + * verbatim. `WorkspaceCommentPaster` is not exported on the Blockly + * namespace, so it cannot simply be reconstructed. + */ +let displacedPaster: Blockly.IPaster< + Blockly.ICopyData, + Blockly.ICopyable +> | null = null; + +/** Replaces core's workspace comment paster with the note-aware one. */ +export function registerNotePaster(): void { + if (registrationCount++) return; + displacedPaster = Blockly.registry.getObject( + Blockly.registry.Type.PASTER, + PASTER_TYPE, + false, + ); + Blockly.clipboard.registry.unregister(PASTER_TYPE); + Blockly.clipboard.registry.register(PASTER_TYPE, new NotePaster()); +} + +/** Restores core's workspace comment paster. */ +export function unregisterNotePaster(): void { + if (--registrationCount > 0) return; + registrationCount = 0; + Blockly.clipboard.registry.unregister(PASTER_TYPE); + if (displacedPaster) { + Blockly.clipboard.registry.register(PASTER_TYPE, displacedPaster); + displacedPaster = null; + } +} diff --git a/blockly-workspace-notes/src/constants/brand.ts b/blockly-workspace-notes/src/constants/brand.ts new file mode 100644 index 0000000..55c4abe --- /dev/null +++ b/blockly-workspace-notes/src/constants/brand.ts @@ -0,0 +1,27 @@ +/** + * @fileoverview The mark that says an object is one of ours. + * + * `utils/guards.ts` used to recognise a note with `instanceof`, which meant it + * had to import both note classes — and that import is what closed the loop + * between `model/note.ts`, `model/stacking.ts` and `utils/guards.ts`. Reading + * a mark off the object instead lets the guard sit at the bottom of the graph + * with nothing beneath it, which is what keeps the whole graph acyclic. + * + * A symbol rather than a string, so it cannot collide with a property a host + * has put on its own comments, and so it stays out of `Object.keys` and + * `JSON.stringify` — which matters here, because `changeNoteProperty_` + * compares note state by stringifying it. + * + * `Symbol.for` rather than `Symbol()`, so two bundled copies of this plugin + * recognise each other's notes. `instanceof` never could: a host that ends up + * with the plugin twice would have had one copy's notes silently vanish from a + * save written by the other. The version segment is the other half of that + * bargain — it stops two copies at *different* versions accepting each other, + * which would be worse. Bump it only when `NoteSurface` changes in a way an + * older copy could not handle. + */ + +/** Marks an object as a note. See the note on `Symbol.for` above. */ +export const NOTE_BRAND = Symbol.for( + '@mit-app-inventor/blockly-workspace-notes/note@1', +); diff --git a/blockly-workspace-notes/src/constants/colours.ts b/blockly-workspace-notes/src/constants/colours.ts new file mode 100644 index 0000000..68efed1 --- /dev/null +++ b/blockly-workspace-notes/src/constants/colours.ts @@ -0,0 +1,82 @@ +/** + * @fileoverview The colour space a note lives in, and the palette it is + * offered from. + * + * The derivations that read these live in `utils/colour.ts`; this file only + * states the numbers. + */ + +import type {PaletteEntry} from '../types/options'; + +/** + * A note is paper: a light colour carrying dark text. + * + * That is the real difference from a block. Blockly's block language is a + * saturated fill with a white label — S 0.45 at V 0.65 via `hueToHex` — and + * anything built that way reads as a block whatever hue it uses. Notes invert + * it: a pale wash at V 0.98, with the text in black. + * + * It is also what Blockly's own comments do. Their default is `#FFFCC7`, a + * pale yellow, and nothing about a comment borrows the block palette. + */ +export const NOTE_SATURATION = 0.25; + +/** The value every note colour is generated at, on a 0-1 scale. */ +export const NOTE_VALUE = 0.98; + +/** Hue of the default note: the yellow a sticky note is expected to be. */ +export const DEFAULT_HUE = 48; + +/** The default note colour. */ +export const DEFAULT_COLOUR = '#f9edbb'; + +/** + * The swatches offered in the "Colour" context menu, in display order. + * + * Stationery colours rather than block colours. Each is stored as the hex the + * palette's saturation and value produce, since that is what a note + * serializes. + */ +export const DEFAULT_PALETTE: PaletteEntry[] = [ + {name: 'Yellow', hue: DEFAULT_HUE, fill: DEFAULT_COLOUR}, + {name: 'Peach', hue: 28, fill: '#f9d8bb'}, + {name: 'Pink', hue: 350, fill: '#f9bbc5'}, + {name: 'Lilac', hue: 275, fill: '#dfbbf9'}, + {name: 'Sky', hue: 200, fill: '#bbe5f9'}, + {name: 'Mint', hue: 150, fill: '#bbf9da'}, + {name: 'Grey', hue: 0, fill: '#f2f2f2'}, +]; + +/** + * How far a note's edge sits below its own colour: the same hue, a step down + * in value. Used for the note's border and for the title bar, which Blockly + * paints from the same `--commentBorderColour` the border reads. + */ +export const EDGE_VALUE_SCALE = 0.88; + +/** + * How the ink is derived from a note's colour: the same hue, taken far darker + * and a good deal more saturated. + * + * Everything written on a note is drawn in it — the title, the body text, the + * three glyphs in the title bar — so it is the one colour that has to stay + * readable on both surfaces a note has, the pale body and the slightly deeper + * bar. Deriving it from the note's own hue rather than reaching for black is + * what makes a green note read as a green note all the way through. + * + * The saturation is scaled up rather than fixed, so a near-grey note keeps + * near-grey text instead of acquiring a colour cast, and capped, so a vivid one + * does not go lurid at this value. + * + * Checked across the palette: the worst contrast these produce is 5.79:1 on the + * bar and 7.67:1 on the body, both clear of the 4.5:1 needed for body text. + * `test/colour.mocha.js` asserts that, so a change here cannot quietly make a + * note unreadable. + */ +export const INK_SATURATION_SCALE = 2.8; + +/** The ceiling on the scaled-up ink saturation. */ +export const INK_SATURATION_MAX = 0.72; + +/** How dark the ink sits, on the 0-1 value scale. */ +export const INK_VALUE = 0.3; diff --git a/blockly-workspace-notes/src/constants/dom.ts b/blockly-workspace-notes/src/constants/dom.ts new file mode 100644 index 0000000..f318494 --- /dev/null +++ b/blockly-workspace-notes/src/constants/dom.ts @@ -0,0 +1,45 @@ +/** + * @fileoverview The class names a note's chrome is marked with, and the one + * piece of text the chrome renders on its own. + * + * The classes are the contract between `model/note.ts`, which adds them, and + * `ui/css.ts`, which styles them. Neither should spell one out inline. + */ + +/** CSS class added to the root SVG group of every note. */ +export const NOTE_CLASS = 'blocklyNote'; + +/** CSS class added to a note that has a non-empty title. */ +export const TITLED_CLASS = 'blocklyNoteTitled'; + +/** CSS class added to a pinned note. */ +export const PINNED_CLASS = 'blocklyNotePinned'; + +/** CSS class of the group holding the marker drawn on a pinned note. */ +export const PIN_CLASS = 'blocklyNotePin'; + +/** CSS class added to a locked note. */ +export const LOCKED_CLASS = 'blocklyNoteLocked'; + +/** CSS class of the group holding the marker drawn on a locked note. */ +export const LOCK_CLASS = 'blocklyNoteLock'; + +/** CSS class of the SVG text element that renders a note's title. */ +export const TITLE_CLASS = 'blocklyNoteTitle'; + +/** CSS class of the group holding the author and date along the note's foot. */ +export const FOOTER_CLASS = 'blocklyNoteFooter'; + +/** CSS class of the two text runs inside the footer. */ +export const FOOTER_TEXT_CLASS = 'blocklyNoteFooterText'; + +/** CSS class of the rect that draws the selection ring. */ +export const SELECTION_CLASS = 'blocklyNoteSelection'; + +/** + * Shown in the title row of a note that has not been named yet. + * + * A note always has a title row, so an unnamed one is labelled rather than + * left blank: the placeholder is what says the row can be clicked. + */ +export const UNTITLED_TITLE_TEXT = 'Title'; diff --git a/blockly-workspace-notes/src/constants/layout.ts b/blockly-workspace-notes/src/constants/layout.ts new file mode 100644 index 0000000..8c86ab2 --- /dev/null +++ b/blockly-workspace-notes/src/constants/layout.ts @@ -0,0 +1,332 @@ +/** + * @fileoverview The geometry a note's chrome is built from. + * + * Every number the note draws is named here, and every one says where it came + * from: Blockly's renderer scale, so a note sits comfortably beside the blocks + * it annotates; `TITLE_LINE_HEIGHT`, the single unit the note's own layout is + * measured in; or core's own stylesheet, where the point is to match it. A + * handful - the fades and the stroke weights - are simply chosen, and say so. + * The rule is that the number is named and sourced, not that it is derived; + * inventing a derivation for a value that has none only hides it better. + * + * The scope is the note's SVG chrome. `ui/colour_swatches.ts` draws HTML + * inside Blockly's context menu and follows core's menu scale instead, which + * is a different set of numbers - 4, 6, 15, 28 - and is deliberately not + * gathered here. + */ + +/** + * Blockly's renderer padding scale, from `renderers/common/constants.ts`. + * Note chrome is measured in these rather than in ad-hoc numbers. + */ +export const SMALL_PADDING = 3; + +/** The middle step of Blockly's padding scale. */ +export const MEDIUM_PADDING = 5; + +/** The step between medium and large. */ +export const MEDIUM_LARGE_PADDING = 8; + +/** The largest step of Blockly's padding scale. */ +export const LARGE_PADDING = 10; + +/** + * Height reserved for one line of title text. + * + * Core's `FIELD_BORDER_RECT_HEIGHT`: the box a single line of field text is + * laid out in, and the closest thing Blockly has to a line box. Not its icon + * size, which is 17. + */ +export const TITLE_LINE_HEIGHT = 16; + +/** + * Type size of the title, in px. + * + * A heading has to be bigger than the text it heads or it is just bold body + * text. Core's field text is 11pt (14.67px) and the body inherits it, so the + * title takes the next size up and lands exactly on `TITLE_LINE_HEIGHT` - the + * line box the layout already reserves for it, which is why the row needs no + * remeasuring to fit it. + */ +export const TITLE_FONT_SIZE = TITLE_LINE_HEIGHT; + +/** + * The box a title bar marker is drawn in. + * + * One line box, so a marked title row is exactly as tall as an unmarked one + * and the row never needs remeasuring. The glyphs are authored on a 24-unit + * grid and scaled to fit, which also thins their 2-unit stroke to about 1.3px + * - about right for a mark that should read as punctuation beside the heading + * rather than as a control. + */ +export const MARKER_ICON_SIZE = TITLE_LINE_HEIGHT; + +/** The gap between a marker and the title it leads. */ +export const MARKER_ICON_GAP = MEDIUM_PADDING; + +/** Where a glyph's ink sits inside the grid it is authored on. */ +export interface GlyphInk { + x: number; + y: number; + width: number; + height: number; +} + +/** + * The grid every glyph in this plugin is authored on. + * + * Tabler draws on 24 units, but none of the glyphs fills that box - the pin + * takes x 7-17, the lock x 5-19 - so a fifth to a third of each one is + * padding. Laying a marker out by that box would set it in from the note's + * margin by the padding and leave a gap to the title wider than was asked for, + * which is exactly the sort of uneven spacing the rest of this layout is built + * to avoid. So the ink box is what gets positioned, and the two below are its + * numbers. + */ +export const GLYPH_GRID = 24; + +/** The pin glyph's ink, in grid units. */ +export const PIN_GLYPH_INK: GlyphInk = {x: 7, y: 4, width: 10, height: 17}; + +/** The lock glyph's ink, in grid units. */ +export const LOCK_GLYPH_INK: GlyphInk = {x: 5, y: 3, width: 14, height: 18}; + +/** + * The margin on every side of a note, and the single number the rest of the + * layout is built from. + * + * A full line of text on every side reads as generous on a sheet of paper, but + * a note is not one any more - it is a titled card with a bar of controls, and + * at that scale the same margin leaves the bar looking inflated and the body + * indented. So it comes back to the top of Blockly's own chrome scale, which + * is what the bar's contents are sized against anyway. + */ +export const NOTE_MARGIN = LARGE_PADDING; + +/** + * Height of a note's title row: one line of text with a margin above and + * below. Core measures this rect and derives the writing area's offset from + * it, so it is what puts the body where it is. + */ +export const TOPBAR_HEIGHT = TITLE_LINE_HEIGHT + NOTE_MARGIN * 2; + +/** + * The box each of core's bar buttons is drawn in. + * + * One line box, the same as the title beside it, so the glyphs and the heading + * share a cap height and the row reads as one line rather than as icons with + * text between them. Smaller than core's 20px, which was sized for a bar + * carrying nothing but icons. + */ +export const BAR_ICON_SIZE = TITLE_LINE_HEIGHT; + +/** The gap between a bar button and whatever it sits next to. */ +export const BAR_ICON_GAP = MEDIUM_PADDING; + +/** + * The inset core leaves around a bar button. + * + * Not ours to choose: `CommentBarButton.getMargin()` computes exactly this, + * half the difference between the bar's height and the icon's, and both + * buttons are positioned with it. Repeating the arithmetic here is what lets + * the title know where they are without measuring them every time it redraws. + */ +export const BAR_ICON_MARGIN = (TOPBAR_HEIGHT - BAR_ICON_SIZE) / 2; + +/** + * How much of each end of the title row a button takes, gap included. + * + * One number for both ends, because the buttons are inset equally once the + * correction below is applied. The pin marker starts here when there is one, + * and the title follows it. + */ +export const BAR_INSET = BAR_ICON_MARGIN + BAR_ICON_SIZE + BAR_ICON_GAP; + +/** + * The inset at an end of the title row that carries no button. + * + * A locked note has no delete button, so the far end of its bar holds nothing + * but the note's own margin. Reserving a whole `BAR_INSET` there would leave + * the title stopping short of a gap with nothing in it. + */ +export const BAR_EMPTY_INSET = NOTE_MARGIN; + +/** + * How far the delete button moves to line up with the collapse button. + * + * Core positions it at the bar's width minus the button's size *including both + * its margins*, while the collapse button gets a single margin from the + * leading edge. So the two sit at different insets - one margin further in on + * the right than on the left - which is plain to see once you look for it. + * Shifting it out by that extra margin makes the row symmetric. + */ +export const BAR_DELETE_NUDGE = BAR_ICON_MARGIN; + +/** + * Width of the body's scrollbar, for engines styled through + * ::-webkit-scrollbar. + * + * A step on Blockly's own scale, and the narrowest one that still leaves the + * thumb something to sit in - narrow enough to read as a mark on the paper + * rather than as a control. + */ +export const SCROLLBAR_WIDTH = MEDIUM_LARGE_PADDING; + +/** Space between a note's edge and its writing area. */ +export const BODY_INSET = NOTE_MARGIN; + +/** + * The size a note is created at when the host sets no `defaultSize`. + * + * Blockly's own comment default is 120x100, which was chosen for a comment + * whose whole chrome is a 24px bar. A note's margins leave that barely two + * lines of text, so notes carry their own default. + */ +export const DEFAULT_SIZE = {width: 260, height: 180}; + +/** + * The strip along the foot of a note carrying its author and date. + * + * One line box again, so the note has the same rhythm top and bottom. The body + * gives up this much height for it - see the padding in `ui/css.ts` - which is + * why it is reserved as a constant rather than drawn wherever it lands. + */ +export const FOOTER_HEIGHT = TITLE_LINE_HEIGHT; + +/** + * The glyphs in the footer, smaller than the bar's since the text is too. + * + * Three quarters of the bar's, which is the ratio the footer type takes + * against the body type - so the two rows shrink by the same step. + */ +export const FOOTER_ICON_SIZE = TITLE_LINE_HEIGHT * 0.75; + +/** + * Footer type size, in px: small enough to read as a caption, not as content. + * + * The same three-quarter step as the glyphs, against a body that inherits + * core's field text - but written out rather than derived, because that step + * is not exact here. Core's size is 11 *pt*, which is 14.667px, and three + * quarters of it is 11.00025. Writing the multiplication would put + * `font-size: 11.00025px` in the stylesheet. + * + * Worth knowing that core's `FIELD_TEXT_FONTSIZE` is also written `11`. The + * two look like the same number and are not: this one is px, that one is pt, + * and the footer is three quarters of the body rather than the same size. + */ +export const FOOTER_FONT_SIZE = 11; + +/** Between a footer glyph and the text it labels. */ +export const FOOTER_LABEL_GAP = SMALL_PADDING; + +/** + * The shortest an author's name is worth showing. + * + * Truncation is what gives the date room on a narrow note, but a name cut to + * one or two letters conveys nothing while still spending a glyph and a gap on + * saying so. Below this the author steps aside entirely. + */ +export const MIN_FOOTER_AUTHOR_CHARS = 3; + +/** Between the author and the date. */ +export const FOOTER_ITEM_GAP = LARGE_PADDING; + +/** + * The box core draws the resize handle in. + * + * Core's own number, from its comment stylesheet, repeated here so the footer + * knows how much room to leave it. It equals `FOOTER_ICON_SIZE` today, which + * is a coincidence and not a reason to share one constant - the footer's + * glyphs could change size without moving core's handle. + */ +export const RESIZE_HANDLE_SIZE = 12; + +/** + * How much of the footer's trailing end the resize handle claims. + * + * The stylesheet pulls the handle a body inset in from the corner, which puts + * it squarely in the footer's row. The footer stops short of it rather than + * running underneath. + */ +export const FOOTER_HANDLE_CLEARANCE = RESIZE_HANDLE_SIZE + LARGE_PADDING; + +/** + * The smallest a note can be resized to. + * + * Core enforces a floor of its own and the larger of the two always wins, so + * this is not the whole story - but it is the half that bites when core's is + * too low. Core's width is the truncated body preview plus whichever bar + * buttons are showing, which on a note with no text yet comes to just the two + * buttons; its height is the bar plus 20px, which leaves less than one line of + * writing under the title. Either would let the resize handle drag a note down + * to something with no surface to grab and no title to read - still there, + * still saved, and findable only by undo. + * + * So a note states its own in the terms the rest of the layout is built from: + * never shorter than its title bar plus one line of body and the margin under + * it, and never narrower than a title of a few characters between its two + * margins. Measured, that means this floor governs an empty note while core's + * takes over once there is enough body text to push past it. + */ +export const MIN_SIZE = { + width: NOTE_MARGIN * 2 + TITLE_LINE_HEIGHT * 4, + height: TOPBAR_HEIGHT + TITLE_LINE_HEIGHT + NOTE_MARGIN, +}; + +/** + * The card's border, and the heavier one a pinned note gets. + * + * One pixel is Blockly's weight for a background edge - core uses it on the + * workspace and the mutator - and doubling it is the whole of the pinned + * note's second signal, beside the marker in its bar. + */ +export const CARD_BORDER_WIDTH = 1; + +/** See `CARD_BORDER_WIDTH`. */ +export const PINNED_CARD_BORDER_WIDTH = CARD_BORDER_WIDTH * 2; + +/** + * The stroke every glyph is authored at, on the 24-unit grid. + * + * Tabler's own weight. It is scaled down with the artwork, so a bar marker + * lands at about 1.3px and a footer glyph at exactly 1 - which is why the two + * rows read as the same family at two sizes rather than as two weights. + */ +export const GLYPH_STROKE_WIDTH = 2; + +/** + * The fade on a placeholder: the untitled heading, and the empty body's + * prompt. + * + * Chosen, not derived. Far enough back to read as "nothing here yet" against + * the note's own ink, and no further, since it still has to be legible on the + * palest paper in the palette. + */ +export const PLACEHOLDER_OPACITY = 0.55; + +/** + * The fade on the footer, which is a caption rather than content. + * + * Matches core's own `.blocklyIconGroup` fade as of v13. It matches rather + * than derives from it - core could move without this following. + */ +export const FOOTER_OPACITY = 0.6; + +/** The focus ring drawn on the bar's buttons, which core styles none for. */ +export const OUTLINE_WIDTH = 2; + +/** See `OUTLINE_WIDTH`. */ +export const OUTLINE_OFFSET = 1; + +/** See `OUTLINE_WIDTH`. Core's radius for a small piece of chrome. */ +export const OUTLINE_RADIUS = 2; + +/** + * The selection ring's weight. + * + * Core's, from `.blocklySelected .blocklyCommentHighlight`. A selected note + * should look selected the same way everything else on the workspace does, so + * this tracks core deliberately - as does the colour, which is left to core's + * own `#fc3` rather than named here. + */ +export const SELECTION_STROKE_WIDTH = 3; diff --git a/blockly-workspace-notes/src/constants/serialization.ts b/blockly-workspace-notes/src/constants/serialization.ts new file mode 100644 index 0000000..0fe6a6d --- /dev/null +++ b/blockly-workspace-notes/src/constants/serialization.ts @@ -0,0 +1,37 @@ +/** + * @fileoverview The names this plugin claims in Blockly's registries, and the + * version of the format it persists. + * + * These are grouped because they are the plugin's public identifiers: change + * one and existing save files, or another plugin's registrations, are affected. + * Everything else in `constants/` only affects how a note looks. + */ + +/** + * The name the note serializer registers under. This doubles as the top-level + * key in the JSON produced by `Blockly.serialization.workspaces.save()`. + */ +export const NOTE_SERIALIZER_NAME = 'workspaceNotes'; + +/** + * The name of Blockly's built-in workspace comment serializer, which we + * replace so that notes are not saved twice. + */ +export const COMMENT_SERIALIZER_NAME = 'workspaceComments'; + +/** + * Current version of the `workspaceNotes` payload. + * + * Bump this when an older file cannot simply be read by the current code — a + * field renamed, retyped, moved or newly required — and add a matching entry + * to MIGRATIONS in `serialization/migrations.ts`. A new optional field is not + * such a change; see the note there. + */ +export const SCHEMA_VERSION = 1; + +/** + * The type string of the custom event used to make note-specific properties + * undoable. Namespaced to avoid colliding with other plugins in the global + * event registry. + */ +export const NOTE_CHANGE_EVENT_TYPE = 'workspace_note_change'; diff --git a/blockly-workspace-notes/src/events/note_change.ts b/blockly-workspace-notes/src/events/note_change.ts new file mode 100644 index 0000000..bf71b4f --- /dev/null +++ b/blockly-workspace-notes/src/events/note_change.ts @@ -0,0 +1,131 @@ +/** + * @fileoverview A custom Blockly event making note-specific properties + * undoable. + * + * Core fires nothing for `setMovable`/`setEditable`/`setDeletable`, and + * `CommentCreate` snapshots a note through core's comment serializer — which + * knows nothing about titles or colours. Without this event, undo would + * silently drop everything the plugin adds. + */ + +import * as Blockly from 'blockly/core'; + +import {NOTE_CHANGE_EVENT_TYPE} from '../constants/serialization'; +import type {NoteChangeJson} from '../types/events'; +import type {NoteProperty, NotePropertyValue} from '../types/note'; + +/** + * The part of a note this event needs in order to replay itself. + * + * Structural rather than importing `Note`: the event module sits below the + * note module in the dependency graph, and only ever calls this one method. + */ +interface NoteLike { + applyNoteProperty(property: NoteProperty, value?: NotePropertyValue): void; +} + +/** + * Notifies listeners that a note-specific property changed. + * + * The `property` is a key understood by `Note.applyNoteProperty()`; `oldValue` + * and `newValue` are the JSON-serializable values on either side of the + * change. The special property `'*'` carries a whole note-state object, which + * is how a note's extra fields survive being deleted and undone. + */ +export class NoteChange extends Blockly.Events.CommentBase { + /** The property that changed, or undefined on a blank event. */ + property?: NoteProperty; + + /** The value before the change. */ + oldValue?: NotePropertyValue; + + /** The value after the change. */ + newValue?: NotePropertyValue; + + /** + * @param comment The note that changed. Undefined for a blank event. + * @param property The property that changed. + * @param oldValue The value before the change. + * @param newValue The value after the change. + */ + constructor( + comment?: Blockly.comments.WorkspaceComment, + property?: NoteProperty, + oldValue?: NotePropertyValue, + newValue?: NotePropertyValue, + ) { + super(comment); + this.type = NOTE_CHANGE_EVENT_TYPE; + this.property = property; + this.oldValue = oldValue; + this.newValue = newValue; + } + + /** + * Encodes the event as JSON. + * @returns JSON representation. + */ + toJson(): NoteChangeJson { + const json = super.toJson() as NoteChangeJson; + json['property'] = this.property; + json['oldValue'] = this.oldValue; + json['newValue'] = this.newValue; + return json; + } + + /** + * Deserializes the JSON event. + * @param json The JSON to decode. + * @param workspace The workspace the event belongs to. + * @param [event] An event to populate, for subclasses. + * @returns The decoded event. + */ + static fromJson( + json: NoteChangeJson, + workspace: Blockly.Workspace, + event?: NoteChange, + ): NoteChange { + const newEvent = super.fromJson( + json, + workspace, + event ?? new NoteChange(), + ) as NoteChange; + newEvent.property = json['property']; + newEvent.oldValue = json['oldValue']; + newEvent.newValue = json['newValue']; + return newEvent; + } + + /** + * Does this event record any change of state? + * + * Values may be objects (for the `'*'` property), so compare structurally. + * Filtering no-ops here keeps them off the undo stack — core's + * `CommentCollapse` omits this and pollutes the stack as a result. + * @returns False if something changed. + */ + isNull(): boolean { + return JSON.stringify(this.oldValue) === JSON.stringify(this.newValue); + } + + /** + * Runs the change event. + * @param forward True to run forward, false to undo. + */ + run(forward: boolean): void { + const workspace = this.getEventWorkspace_(); + const note = this.commentId + ? (workspace.getCommentById(this.commentId) as NoteLike | null) + : null; + if (!note || typeof note.applyNoteProperty !== 'function') { + // Matches core's tolerance for replaying against a vanished object. + console.warn(`Can't change non-existent note: ${this.commentId}`); + return; + } + if (!this.property) return; + note.applyNoteProperty( + this.property, + forward ? this.newValue : this.oldValue, + ); + } +} diff --git a/blockly-workspace-notes/src/events/registry.ts b/blockly-workspace-notes/src/events/registry.ts new file mode 100644 index 0000000..7823f78 --- /dev/null +++ b/blockly-workspace-notes/src/events/registry.ts @@ -0,0 +1,28 @@ +/** + * @fileoverview Teaching Blockly's event registry about `NoteChange`. + * + * Separate from the event itself so that importing the class — to construct + * one, or to name it in a type — never has the side effect of registering it. + */ + +import * as Blockly from 'blockly/core'; + +import {NOTE_CHANGE_EVENT_TYPE} from '../constants/serialization'; +import {NoteChange} from './note_change'; + +/** + * Registers NoteChange so `Blockly.Events.fromJson` can rebuild it. Safe to + * call repeatedly; re-registering the identical class is a no-op in Blockly's + * registry, and a duplicate-name throw would only mean it is already present. + */ +export function registerNoteChangeEvent(): void { + try { + Blockly.registry.register( + Blockly.registry.Type.EVENT, + NOTE_CHANGE_EVENT_TYPE, + NoteChange, + ); + } catch { + // Already registered by another copy of the plugin. + } +} diff --git a/blockly-workspace-notes/src/index.ts b/blockly-workspace-notes/src/index.ts new file mode 100644 index 0000000..f542e26 --- /dev/null +++ b/blockly-workspace-notes/src/index.ts @@ -0,0 +1,60 @@ +/** + * @fileoverview A Blockly plugin adding sticky-note style workspace comments + * with first-class JSON serialization. + * + * Notes extend Blockly's own workspace comments — so dragging, resizing, + * collapsing, selection, keyboard navigation and undo all come for free — and + * add a title, a colour, authorship metadata and a stacking order. They are + * saved under their own versioned `workspaceNotes` key alongside `blocks`, and + * older `workspaceComments` files still load. + * + * This file is the package's whole public surface and holds no logic of its + * own: anything not re-exported here is internal, and free to move. It is also + * the build entry point, which `@blockly/dev-scripts` resolves by path, so it + * stays at `src/index.ts`. + * + * @example + * import * as Blockly from 'blockly'; + * import {WorkspaceNotes} from '@mit-app-inventor/blockly-workspace-notes'; + * + * const workspace = Blockly.inject('blocklyDiv', {toolbox}); + * const notes = new WorkspaceNotes(workspace); + * notes.init(); + */ + +// The plugin itself: what a host application constructs. +export {WorkspaceNotes} from './plugin'; + +// The note classes, and how to recognise and order them. +export {Note} from './model/note'; +export {NoteComment} from './model/note_comment'; +export {restackNotes} from './model/stacking'; +export {isNote} from './utils/guards'; + +// Persistence, for a host that drives saving and loading itself. +export {LegacyCommentAdapter} from './serialization/legacy_comment_adapter'; +export {migrate} from './serialization/migrations'; +export {NoteSerializer} from './serialization/note_serializer'; +export {appendNote, saveNote} from './serialization/state'; +export {domToNote, domToNoteState, noteToDom} from './serialization/xml/dom'; + +// The undo event and the paster, for a host extending either. +export {NoteChange} from './events/note_change'; +export {NotePaster} from './clipboard/note_paster'; + +// The handful of constants a host is expected to read. +export {DEFAULT_COLOUR, DEFAULT_PALETTE} from './constants/colours'; +export {NOTE_SERIALIZER_NAME, SCHEMA_VERSION} from './constants/serialization'; + +export type {NoteCopyData} from './types/clipboard'; +export type {NoteChangeJson} from './types/events'; +export type { + NoteMeta, + NoteProperty, + NotePropertyValue, + NoteState, + NoteSurface, +} from './types/note'; +export type {NotesPayload, SavedNote} from './types/serialization'; +export type {LockPredicate} from './ui/lock_permission'; +export type {PaletteEntry, WorkspaceNotesOptions} from './types/options'; diff --git a/blockly-workspace-notes/src/model/default_size.ts b/blockly-workspace-notes/src/model/default_size.ts new file mode 100644 index 0000000..60c3b1d --- /dev/null +++ b/blockly-workspace-notes/src/model/default_size.ts @@ -0,0 +1,52 @@ +/** + * @fileoverview The size a new note is created at. + * + * Blockly reads this from `CommentView.defaultCommentSize`, a static on core, + * when it builds a comment with no size of its own. Core's value is 120x100 — + * sized for a comment whose whole chrome is a 24px bar, which a note's margins + * would leave barely two lines of writing. + * + * Because it is a static rather than anything owned by a workspace, it is + * swapped the way this plugin swaps every other global: reference-counted, so + * core's own value comes back only once the last plugin instance is gone. Two + * workspaces on one page therefore share one default — the first one to start + * wins, the same rule `registerNoteSerializers` applies to its options — and, + * more importantly, disposing one plugin cannot resize the notes of another + * that is still running. + */ + +import * as Blockly from 'blockly/core'; + +/** How many plugin instances are currently holding the swap. */ +let registrationCount = 0; + +/** Core's own default, kept so it can be put back exactly as it was. */ +let displacedSize: Blockly.utils.Size | null = null; + +/** + * Makes new notes default to the given size. + * + * @param size The size a note with no saved size should be created at. + * @param size.width Width in workspace units. + * @param size.height Height in workspace units. + */ +export function applyDefaultNoteSize(size: { + width: number; + height: number; +}): void { + if (registrationCount++) return; + displacedSize = Blockly.comments.CommentView.defaultCommentSize; + Blockly.comments.CommentView.defaultCommentSize = new Blockly.utils.Size( + size.width, + size.height, + ); +} + +/** Restores the default size core shipped with. */ +export function restoreDefaultNoteSize(): void { + if (--registrationCount > 0) return; + registrationCount = 0; + if (!displacedSize) return; + Blockly.comments.CommentView.defaultCommentSize = displacedSize; + displacedSize = null; +} diff --git a/blockly-workspace-notes/src/model/note.ts b/blockly-workspace-notes/src/model/note.ts new file mode 100644 index 0000000..de1ccf4 --- /dev/null +++ b/blockly-workspace-notes/src/model/note.ts @@ -0,0 +1,693 @@ +/** + * @fileoverview A note on a rendered workspace: the chrome drawn over the + * state `model/note_mixin.ts` defines. + * + * Everything here is SVG the note adds to, or takes away from, the comment + * core already built — the card, the title, and the markers that show a note + * is pinned or locked — plus the plumbing that keeps that chrome in step with + * a comment core is resizing, collapsing and dragging underneath it. + */ + +import * as Blockly from 'blockly/core'; + +import { + NOTE_CLASS, + FOOTER_CLASS, + FOOTER_TEXT_CLASS, + LOCK_CLASS, + LOCKED_CLASS, + PIN_CLASS, + SELECTION_CLASS, + PINNED_CLASS, + TITLED_CLASS, + TITLE_CLASS, + UNTITLED_TITLE_TEXT, +} from '../constants/dom'; +import { + BAR_EMPTY_INSET, + BAR_INSET, + BODY_INSET, + FOOTER_HANDLE_CLEARANCE, + FOOTER_HEIGHT, + FOOTER_ICON_SIZE, + FOOTER_ITEM_GAP, + FOOTER_LABEL_GAP, + MIN_FOOTER_AUTHOR_CHARS, + MIN_SIZE, + GLYPH_GRID, + LOCK_GLYPH_INK, + MARKER_ICON_GAP, + MARKER_ICON_SIZE, + PIN_GLYPH_INK, + TOPBAR_HEIGHT, +} from '../constants/layout'; +import type {GlyphInk} from '../constants/layout'; +import type {NoteCopyData} from '../types/clipboard'; +import {edgeFor, inkFor} from '../utils/colour'; +import { + CALENDAR_GLYPH, + CHEVRON_GLYPH, + LOCK_GLYPH, + PIN_GLYPH, + TRASH_GLYPH, + USER_GLYPH, + glyphToDataUri, +} from '../ui/icons'; +import {editTitle} from '../ui/title_editor'; +import {msg} from '../utils/messages'; +import {RenderedNoteBase} from './note_mixin'; +import {restackNotes} from './stacking'; + +/** + * A note on a rendered workspace. + */ +export class Note extends RenderedNoteBase { + /** + * The paper card behind the note's chrome. + * + * Every field here is optional, and that is not defensiveness: they are + * assigned after `super()` returns, and `super()` can call back into + * `renderTitle()` and `renderColour()`, which read them. The guard in + * `renderTitle` exists for exactly that window. + */ + private card_?: SVGRectElement | null; + + /** Where a press on the title started, while one is in progress. */ + private titlePressPoint_?: {x: number; y: number} | null; + + /** The SVG text element holding the title. */ + private titleElement_?: SVGTextElement; + + /** The marker shown while the note is pinned. */ + private pin_?: SVGGElement; + + /** The marker shown while the note is locked. */ + private lock_?: SVGGElement; + + /** The rect that draws the selection ring. */ + private selection_?: SVGRectElement; + + /** The footer group, and the four pieces laid out inside it. */ + private footer_?: SVGGElement; + + /** See `footer_`. */ + private authorIcon_?: SVGGElement; + + /** See `footer_`. */ + private authorText_?: SVGTextElement; + + /** See `footer_`. */ + private dateIcon_?: SVGGElement; + + /** See `footer_`. */ + private dateText_?: SVGTextElement; + + /** + * Core's two bar buttons. + * + * Kept only so their artwork can be retinted when the note is recoloured. + * Everything else about them - where they sit, what they do, their focus and + * ARIA - stays core's business. + */ + private foldoutIcon_?: SVGImageElement | null; + + /** See `foldoutIcon_`. */ + private deleteIcon_?: SVGImageElement | null; + + /** The text node inside `titleElement_`. */ + private titleNode_?: Text; + + /** Watches the comment's size so the chrome can follow it. */ + private sizeObserver_?: MutationObserver; + + /** + * @param workspace The workspace to add the note to. + * @param id An optional ID; generated when omitted. + */ + constructor(workspace: Blockly.WorkspaceSvg, id?: string) { + super(workspace, id); + + const root = this.getSvgRoot(); + Blockly.utils.dom.addClass(root, NOTE_CLASS); + const topBar = root.querySelector('.blocklyCommentTopbar'); + this.foldoutIcon_ = root.querySelector('.blocklyFoldoutIcon'); + this.deleteIcon_ = root.querySelector('.blocklyDeleteIcon'); + + /** + * The card itself: core's own highlight rect, which a note fills rather + * than drawing paper of its own. Core resizes it on every pointer move of + * a drag and strokes it when the note is selected, so borrowing it keeps + * both for free. + * + * Square, like Blockly's own comment: core leaves this rect unrounded and + * a note no longer overrides that. + * @private + */ + this.card_ = root.querySelector('.blocklyCommentHighlight'); + + /** + * The two title bar markers: Tabler's `pinned` and `lock`, both authored + * on a 24-unit grid, and both drawn into the same slot between the + * collapse button and the title. The stylesheet shows at most one, so the + * slot never carries two and the title's offset stays a single term. + * + * They live in the root group, *not* in core's top bar. Core mirrors that + * bar wholesale in RTL and each of its children has to undo the mirror for + * itself; the root group is not mirrored, so a marker flips its own + * coordinates instead. Both are inserted after the bar in document order + * so they paint on top of it - the bar is opaque, and anything before it + * is simply covered. + * + * They differ in what they tell assistive technology. The pin is + * decoration: pinning takes away one thing, the menu says so plainly, and + * anyone can undo it in a click. Locking takes away four at once, removes + * a visible button from the bar, and in the case this was built for cannot + * be undone by the person reading it - and core's only signal is the + * readonly attribute on the body, which is reached after focus is already + * inside it and says nothing about the title or the missing bin. So the + * lock is labelled and the pin is not. + * @private + */ + let after: Element | null = topBar; + const marker = ( + cls: string, + paths: string[], + aria: Record, + ) => { + const g = Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.G, { + 'class': cls, + ...aria, + }); + for (const d of paths) { + Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.PATH, {'d': d}, g); + } + if (after) { + root.insertBefore(g, after.nextSibling); + after = g; + } else { + root.appendChild(g); + } + return g; + }; + this.pin_ = marker(PIN_CLASS, PIN_GLYPH, {'aria-hidden': 'true'}); + this.lock_ = marker(LOCK_CLASS, LOCK_GLYPH, { + 'role': 'img', + 'aria-label': msg('NOTE_LOCKED_LABEL', 'Locked'), + }); + + /** + * The footer: who wrote the note, and when it last changed. + * + * Both are recorded on every note already and neither was ever shown. It + * sits along the foot rather than in the title bar because the bar is a + * row of controls and this is a caption - and because the body can give up + * a line at the bottom, where the bar has no room to give. + * @private + */ + this.footer_ = Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.G, + {'class': FOOTER_CLASS, 'aria-hidden': 'true'}, + root, + ); + const glyph = (paths: string[]) => { + const g = Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.G, + {}, + this.footer_, + ); + for (const d of paths) { + Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.PATH, {'d': d}, g); + } + return g; + }; + const label = () => + Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.TEXT, + {'class': `${FOOTER_TEXT_CLASS} blocklyText`}, + this.footer_, + ); + this.authorIcon_ = glyph(USER_GLYPH); + this.authorText_ = label(); + this.dateIcon_ = glyph(CALENDAR_GLYPH); + this.dateText_ = label(); + + /** + * The selection ring. + * + * A rect of its own rather than a stroke on the card, because the card is + * painted first and the title bar is painted over it: a stroke there is + * half-covered for the bar's whole height and full thickness below, which + * reads as a ring that changes width halfway down. Drawn last, it is even + * the whole way round. + * @private + */ + this.selection_ = Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.RECT, + {'class': SELECTION_CLASS, 'aria-hidden': 'true'}, + root, + ); + /** + * The SVG text element showing the title above the writing area. + * @private + */ + this.titleElement_ = Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.TEXT, + {'class': `${TITLE_CLASS} blocklyText`}, + topBar, + ); + + /** + * The text node holding the (possibly truncated) title. + * @private + */ + this.titleNode_ = document.createTextNode(''); + this.titleElement_.appendChild(this.titleNode_); + + // A field on a block opens its editor on a single click, and the title + // does the same. Gesture decides a field click by checking the press + // never travelled further than the drag radius, so this repeats that test + // rather than claiming every press: dragging a note by its title has to + // keep working. + // + // Bound directly rather than through browserEvents.conditionalBind, which + // gates on Blockly's own touch handling. + this.titleElement_.addEventListener('pointerdown', (e) => { + this.titlePressPoint_ = {x: e.clientX, y: e.clientY}; + }); + + this.titleElement_.addEventListener('pointerup', (e) => { + const press = this.titlePressPoint_; + this.titlePressPoint_ = null; + // The editable check is also what keeps a locked note's title from + // opening, which is why the inline editor itself needs no lock handling. + if (!press || !this.isEditable()) return; + const travelled = Math.hypot(e.clientX - press.x, e.clientY - press.y); + if (travelled > Blockly.config.dragRadius) return; + + // Deferred by a task, and deliberately not stopped. Core's gesture ends + // on this same release and focuses the note's root, so an editor opened + // inline would have focus taken straight back off it; and Gesture binds + // the release on the document, so stopping the event here would leave + // that gesture running. + setTimeout(() => editTitle(this), 0); + }); + + /** + * Watches the view's own width and height attributes. + * + * Core resizes through `setSizeWithoutFiringEvents` on every pointer move + * of a resize drag and only fires its size listeners once, on pointer up. + * Laying the title out from those listeners alone would leave it + * truncated to the old width for the whole drag, so track the attributes + * core writes instead - they change on every move. + * @private + */ + this.sizeObserver_ = new MutationObserver(() => this.renderChrome()); + this.sizeObserver_.observe(root, { + attributes: true, + attributeFilter: ['width', 'height'], + }); + this.view.addDisposeListener(() => this.sizeObserver_?.disconnect()); + + this.view.addOnCollapseListener(() => this.renderChrome()); + + this.renderColour(); + this.renderChrome(); + + // Every size a note is ever given passes through here: core's resize drag + // calls this on each pointer move, `setSize` calls it, and so does + // collapsing (with the stored size, which is why clamping cannot disturb + // it). Core's own floor is computed in a private method a plugin has no + // business replacing, and it is the wrong floor anyway - see MIN_SIZE. + const setViewSize = this.view.setSizeWithoutFiringEvents.bind(this.view); + this.view.setSizeWithoutFiringEvents = (size: Blockly.utils.Size) => { + setViewSize( + Blockly.utils.Size.max( + size, + new Blockly.utils.Size(MIN_SIZE.width, MIN_SIZE.height), + ), + ); + }; + + // Core sizes the note once from its own constructor, and derives the + // writing area's offset there from the measured height of the top bar + // rect. That measurement happens before `super()` returns, which is + // before this constructor can add NOTE_CLASS - so the rect is still core's + // own 24px bar, and the body is left starting too high, overlapping the + // title row. It corrected itself on the first resize, collapse or + // keystroke, which is what made it look like a rendering glitch rather + // than a wrong number. + // + // The class is on the root by now, so one more size pass measures the + // note's own bar height and puts the body below it. Without firing events: + // the note is still being constructed, and a size change nobody made does + // not belong on the undo stack. + this.view.setSizeWithoutFiringEvents(this.view.getSize()); + } + + /** + * Redraws everything this plugin lays out over core's comment view: the + * card's height and the title. + */ + renderChrome() { + if (this.isDeadOrDying()) return; + this.renderCard(); + this.renderTitle(); + this.renderFooter(); + } + + /** + * Shrinks the card to the top bar when the note is collapsed. + * + * Core leaves its highlight rect at the expanded size whatever the + * collapsed state - `updateHighlightRect` is always passed `this.size` - + * because the rect is `fill: none` for a plain comment and therefore never + * seen. A note fills it, so left alone a collapsed note would sit there as + * a full-height card with an empty body. `view.getSize()` is the + * collapse-aware measurement, so the height is taken from that instead. + */ + renderCard() { + const {width, height} = this.view.getSize(); + this.card_?.setAttribute('height', `${height}`); + + // The ring traces the card, so it takes the same box - including core's + // convention of hanging the card off the leading edge in RTL. + this.selection_?.setAttribute('x', `${this.workspace.RTL ? -width : 0}`); + this.selection_?.setAttribute('y', '0'); + this.selection_?.setAttribute('width', `${width}`); + this.selection_?.setAttribute('height', `${height}`); + } + + /** + * Paints the note by overriding the CSS custom properties core's comment + * stylesheet already reads, which avoids restyling its elements directly. + * + * Three values off the one stored colour: the body, the edge that draws both + * the border and the title bar, and the ink that everything written on the + * note is drawn in. The ink is the one core has no property for, so it also + * has to be painted into the bar buttons by hand - see below. + */ + renderColour() { + const colour = this.getColour(); + const ink = inkFor(colour); + const style = this.getSvgRoot().style; + style.setProperty('--commentFillColour', colour); + style.setProperty('--commentBorderColour', edgeFor(colour)); + style.setProperty('--noteInkColour', ink); + + // Core's bar buttons are elements, which no amount of CSS can + // tint, so the artwork is swapped for one already drawn in this note's + // ink. Core reads nothing back off them but their bounding box, id and + // visibility - none of which the picture affects - so the buttons stay + // entirely core's, keyboard handling and all. + this.foldoutIcon_?.setAttribute('href', glyphToDataUri(CHEVRON_GLYPH, ink)); + this.deleteIcon_?.setAttribute('href', glyphToDataUri(TRASH_GLYPH, ink)); + } + + /** + * Draws the title and its marker, truncating the title to what is left of + * the width. + * + * Called on every size change as well as every title change, since the + * truncation depends on both - and on pinning and locking, either of which + * moves the title over to make room for a marker. + */ + renderTitle() { + if (!this.titleElement_ || this.isDeadOrDying()) return; + + const named = !!this.getTitle(); + Blockly.utils.dom[named ? 'addClass' : 'removeClass']( + this.getSvgRoot(), + TITLED_CLASS, + ); + + // An unnamed note still shows a title row - the placeholder is what says + // the row can be clicked - so there is always text to lay out. The + // stylesheet greys it. + const title = named ? this.getTitle() : UNTITLED_TITLE_TEXT; + if (!this.titleNode_ || !this.titleElement_) return; + this.titleNode_.textContent = title; + + // The title is boxed in on both sides: the collapse button before it, the + // delete button after it, and the pin marker in between when there is one. + // Core makes room for none of that - `calcMinSize` sums only its own two + // buttons, and nothing at all knows about the marker - so these insets are + // the only thing keeping the four from overlapping. + // One marker slot, and a lock takes it from a pin: a note that cannot be + // edited is the more urgent of the two facts, and the pin comes back the + // moment it is unlocked. So the leading offset stays one term however many + // states the note is in at once. + const marker = this.isLocked() + ? LOCK_GLYPH_INK + : this.isPinned() + ? PIN_GLYPH_INK + : null; + const leading = BAR_INSET + (marker ? markerAdvance(marker) : 0); + + // Inside core's top bar group, which it mirrors in RTL - so x counts + // inwards from the note's edge either way, and the sign follows. + const dir = this.workspace.RTL ? -1 : 1; + this.positionMarker_(this.pin_, PIN_GLYPH_INK, dir); + this.positionMarker_(this.lock_, LOCK_GLYPH_INK, dir); + + this.titleElement_.setAttribute('x', `${dir * leading}`); + this.titleElement_.setAttribute('y', `${TOPBAR_HEIGHT / 2}`); + + // A locked note has no delete button, so the far end of its bar holds + // nothing but the margin. Reserving a button's worth there would leave the + // title stopping short of an empty gap. + const trailing = this.isLocked() ? BAR_EMPTY_INSET : BAR_INSET; + + // Trim a character at a time; titles are short, so this settles fast. + const maxWidth = Math.max( + 0, + this.view.getSize().width - leading - trailing, + ); + let text = title; + while ( + text.length > 1 && + Blockly.utils.dom.getTextWidth(this.titleElement_) > maxWidth + ) { + text = text.slice(0, -1); + this.titleNode_.textContent = `${text}\u2026`; + } + } + + /** + * Lays out the author and the date along the foot of the note. + * + * Both are read from the note's own metadata, so a host that supplies no + * `getAuthor` gets the date alone and the row shortens to match. The date is + * `updatedAt` rather than `createdAt`: on a working note the useful question + * is whether it is still current. + * + * The row stops short of the resize handle, which core puts in this same + * corner, and the author is what gives way when there is not enough width - + * the date is short and fixed, so truncating it would save nothing. + */ + renderFooter() { + if (!this.footer_ || !this.authorText_ || !this.dateText_) return; + if (!this.authorIcon_ || !this.dateIcon_) return; + + const {author, updatedAt} = this.getMeta(); + const date = formatDate(updatedAt); + // Nothing worth a row: let the stylesheet's :empty rule hide it. + this.footer_.setAttribute('data-empty', author || date ? 'false' : 'true'); + + const dir = this.workspace.RTL ? -1 : 1; + const {width, height} = this.view.getSize(); + const y = height - BODY_INSET - FOOTER_HEIGHT / 2; + const scale = FOOTER_ICON_SIZE / GLYPH_GRID; + + // `offset` is measured from the note's leading edge. The footer sits in + // the root group, which core does not mirror - unlike the title bar - so + // nothing here flips itself. In RTL the local space runs from -width to 0, + // so an offset becomes a negative coordinate and the glyph is placed by + // its far edge; the artwork keeps its own orientation either way, which a + // calendar or a person very much needs. + const place = ( + icon: SVGGElement, + text: SVGTextElement, + value: string, + offset: number, + ) => { + const shown = !!value; + icon.style.display = shown ? '' : 'none'; + text.style.display = shown ? '' : 'none'; + if (!shown) return 0; + const iconX = dir > 0 ? offset : -(offset + FOOTER_ICON_SIZE); + icon.setAttribute( + 'transform', + `translate(${iconX}, ${y - FOOTER_ICON_SIZE / 2}) scale(${scale})`, + ); + const textOffset = offset + FOOTER_ICON_SIZE + FOOTER_LABEL_GAP; + text.setAttribute('x', `${dir * textOffset}`); + text.setAttribute('y', `${y}`); + text.textContent = value; + return ( + FOOTER_ICON_SIZE + + FOOTER_LABEL_GAP + + Blockly.utils.dom.getTextWidth(text) + ); + }; + + // The date is placed first so its width is known, then the author is given + // whatever is left. + const room = Math.max(0, width - BODY_INSET * 2 - FOOTER_HANDLE_CLEARANCE); + this.dateText_.textContent = date; + const dateWidth = date + ? FOOTER_ICON_SIZE + + FOOTER_LABEL_GAP + + Blockly.utils.dom.getTextWidth(this.dateText_) + : 0; + + let authorLabel = author; + const authorRoom = + room - dateWidth - (date && author ? FOOTER_ITEM_GAP : 0); + if (authorLabel) { + let trimmed = author; + this.authorText_.textContent = authorLabel; + while ( + trimmed.length > 1 && + FOOTER_ICON_SIZE + + FOOTER_LABEL_GAP + + Blockly.utils.dom.getTextWidth(this.authorText_) > + authorRoom + ) { + trimmed = trimmed.slice(0, -1); + authorLabel = `${trimmed}\u2026`; + this.authorText_.textContent = authorLabel; + } + // A name cut to a letter or two says nothing and still spends a glyph + // and a gap saying it, so below that the author gives up its place. + if (trimmed.length < MIN_FOOTER_AUTHOR_CHARS) authorLabel = ''; + } + + let x = BODY_INSET; + const used = place(this.authorIcon_, this.authorText_, authorLabel, x); + if (used) x += used + FOOTER_ITEM_GAP; + place(this.dateIcon_, this.dateText_, date, x); + } + + /** + * Places a marker in the slot between the collapse button and the title. + * + * Both markers are positioned unconditionally, hidden or not: the stylesheet + * decides which one paints, and writing one attribute to a hidden group is + * cheaper than working out whether it was worth skipping. + * + * The glyphs are authored on a 24-unit grid, so they are scaled down to one + * line box. In RTL the whole top bar is mirrored by core, and the negative + * scale undoes that for the glyph itself - the same correction the title + * gets from the stylesheet. + * + * @param el The marker group, if it has been built yet. + * @param ink Where the glyph's ink sits in its grid. + * @param dir 1 in a left-to-right workspace, -1 in a right-to-left one. + */ + private positionMarker_( + el: SVGGElement | undefined, + ink: GlyphInk, + dir: number, + ) { + if (!el) return; + const scale = MARKER_ICON_SIZE / GLYPH_GRID; + // Offset by the glyph's own padding so it is the ink that starts where the + // title row's contents do, rather than the empty box around it. + const x = dir * (BAR_INSET - ink.x * scale); + const y = TOPBAR_HEIGHT / 2 - (ink.y + ink.height / 2) * scale; + el.setAttribute( + 'transform', + `translate(${x}, ${y}) scale(${dir * scale}, ${scale})`, + ); + } + + /** Holds or releases the note, and marks it visually. */ + applyPinned() { + super.applyPinned(); + const pinned = this.isPinned(); + Blockly.utils.dom[pinned ? 'addClass' : 'removeClass']( + this.getSvgRoot(), + PINNED_CLASS, + ); + // The marker takes room from the title, so the row has to be laid out + // again. Nothing else would do it until the next resize. + this.renderTitle(); + if (pinned) this.applyZIndex(); + } + + /** Marks the note read-only, and gives the lock the marker slot. */ + applyLocked() { + super.applyLocked(); + Blockly.utils.dom[this.isLocked() ? 'addClass' : 'removeClass']( + this.getSvgRoot(), + LOCKED_CLASS, + ); + // Mandatory, not defensive. Locking changes which marker shows and takes + // the delete button out of the bar, but `setEditable` fires no event and + // touches no size attribute, so the size observer never wakes and nothing + // else would lay the row out again. + this.renderTitle(); + } + + /** Restacks every note on the workspace to match their z-indices. */ + applyZIndex() { + restackNotes(this.workspace); + } + + /** Raises this note above its neighbours, by moving it in the DOM. */ + bringToFront() { + this.view.bringToFront(); + } + + /** + * Includes the note's extra state in clipboard data so that duplicate and + * paste keep the title and colour. Consumed by NotePaster. + * @returns The copy data, or null if the note is not copyable. + */ + toCopyData(): NoteCopyData | null { + // Core's own version does not check this, so without it a locked note + // could still be copied by anything reaching past the context menu. + if (!this.isCopyable()) return null; + const data = super.toCopyData() as NoteCopyData | null; + if (!data) return null; + data.noteState = this.saveNoteState(); + return data; + } +} + +/** + * How much of the title row a marker takes, its gap included. + * + * Measured across the ink rather than the glyph's box, matching where + * `positionMarker_` puts it - the two have to agree or the title either + * collides with the marker or floats away from it. + * + * @param ink Where the glyph's ink sits in its grid. + * @returns The width to give the marker, in workspace units. + */ +function markerAdvance(ink: GlyphInk): number { + return (ink.width * MARKER_ICON_SIZE) / GLYPH_GRID + MARKER_ICON_GAP; +} + +/** + * Renders a stored timestamp as a short, local date. + * + * The metadata holds ISO 8601 strings so they survive a round trip verbatim; + * this is only for reading. An unparseable or absent value yields an empty + * string, which the footer treats as "nothing to show" rather than printing + * "Invalid Date" on the note. + * + * @param iso An ISO 8601 timestamp, or an empty string. + * @returns The date in the viewer's locale, or '' if there is not one. + */ +function formatDate(iso: string): string { + if (!iso) return ''; + const date = new Date(iso); + if (isNaN(date.getTime())) return ''; + return date.toLocaleDateString(undefined, { + day: 'numeric', + month: 'short', + year: 'numeric', + }); +} diff --git a/blockly-workspace-notes/src/model/note_comment.ts b/blockly-workspace-notes/src/model/note_comment.ts new file mode 100644 index 0000000..8ce15d1 --- /dev/null +++ b/blockly-workspace-notes/src/model/note_comment.ts @@ -0,0 +1,14 @@ +/** + * @fileoverview A note on a headless workspace. + * + * This is the class a Node test gets, and the one the serializers build when + * no renderer exists. It carries the whole note state and none of the chrome; + * `model/note.ts` is the same thing with a card drawn around it. + */ + +import {NoteCommentBase} from './note_mixin'; + +/** + * A note on a headless workspace: all of the state, none of the rendering. + */ +export class NoteComment extends NoteCommentBase {} diff --git a/blockly-workspace-notes/src/model/note_mixin.ts b/blockly-workspace-notes/src/model/note_mixin.ts new file mode 100644 index 0000000..dba3ec0 --- /dev/null +++ b/blockly-workspace-notes/src/model/note_mixin.ts @@ -0,0 +1,416 @@ +/** + * @fileoverview The note behaviour, as a mixin over a workspace comment class. + * + * Blockly keeps its comment model and its rendered comment in a single + * inheritance chain (`WorkspaceComment` -> `RenderedWorkspaceComment`), and a + * headless workspace only ever produces the former. The extra state is + * therefore expressed as a mixin and applied to both, so notes work + * identically in a browser and in a headless workspace (which is all a Node + * test can build — `Blockly.inject` is unavailable there). + * + * Everything here works without a DOM. The four `render*`/`apply*` methods are + * the seam: they are no-ops at this level, and `model/note.ts` overrides them + * to paint the same state onto SVG. + */ + +import * as Blockly from 'blockly/core'; + +import {NOTE_BRAND} from '../constants/brand'; +import {DEFAULT_COLOUR} from '../constants/colours'; +import {NoteChange} from '../events/note_change'; +import {asOneUndoStep} from '../utils/undo'; +import type { + NoteMeta, + NoteProperty, + NotePropertyValue, + NoteState, + NoteSurface, +} from '../types/note'; + +/** + * Any constructor producing a workspace comment. + * + * `any[]` rather than the real parameters because a mixin cannot know what its + * base takes; the two exported classes below restore the real signature. + */ +type CommentConstructor = new ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...args: any[] +) => Blockly.comments.WorkspaceComment; + +/** + * Adds note state and behaviour to a workspace comment class. + * + * @param Base `WorkspaceComment` or `RenderedWorkspaceComment`. + * @returns The extended class. + */ +const NoteMixin = (Base: TBase) => + class extends Base { + /** + * This note's extra state, created on first access. + * + * Declared without an initializer on purpose. `target: es6` keeps + * `useDefineForClassFields` off, so this declaration emits nothing — which + * matters, because a field definition would run after `super()` and wipe a + * value that the base constructor's create event had already caused to be + * built. + */ + protected noteState_?: NoteState; + + /** + * Marks this object as a note, for `utils/guards.isNote`. + * + * An accessor rather than a class field, so it lives on the prototype: it + * exists from the moment the class does, costs nothing per instance, and + * is non-enumerable where an own field would not be - which keeps it out + * of the `JSON.stringify` comparison in `changeNoteProperty_`. + * + * @returns Always true. + */ + get [NOTE_BRAND](): true { + return true; + } + + /** + * Returns this note's extra state, creating it on first access. + * + * The state is built lazily rather than in a class field because subclass + * fields are initialized only after `super()` returns, and the + * `WorkspaceComment` constructor already fires a create event and drives + * view setup before that point. + * + * @returns The live state object. Treat as read-only. + */ + getNoteState(): NoteState { + if (!this.noteState_) { + const now = new Date().toISOString(); + this.noteState_ = { + title: '', + colour: DEFAULT_COLOUR, + pinned: false, + locked: false, + zIndex: 0, + meta: {author: '', createdAt: now, updatedAt: now}, + }; + } + return this.noteState_; + } + + /** @returns The note's title, or '' if it has none. */ + getTitle(): string { + return this.getNoteState().title; + } + + /** + * Sets the note's title. + * @param title The new title. + */ + setTitle(title: string): void { + this.changeNoteProperty_('title', String(title ?? '')); + } + + /** @returns The note's background colour as a hex string. */ + getColour(): string { + return this.getNoteState().colour; + } + + /** + * Sets the note's background colour. + * @param colour A CSS colour; parsed to hex via Blockly. + */ + setColour(colour: string): void { + const parsed = Blockly.utils.colour.parse(colour) ?? DEFAULT_COLOUR; + this.changeNoteProperty_('colour', parsed); + } + + /** @returns Whether the note is pinned. */ + isPinned(): boolean { + return this.getNoteState().pinned; + } + + /** + * Pins or unpins the note. A pinned note is held in place and kept in + * front of its neighbours. + * @param pinned Whether the note should be pinned. + */ + setPinned(pinned: boolean): void { + this.changeNoteProperty_('pinned', !!pinned); + } + + /** @returns Whether the note is locked. */ + isLocked(): boolean { + return this.getNoteState().locked; + } + + /** + * Locks or unlocks the note. A locked note is read-only and cannot be + * deleted, but can still be moved, collapsed and restacked. + * @param locked Whether the note should be locked. + */ + setLocked(locked: boolean): void { + this.changeNoteProperty_('locked', !!locked); + } + + /** @returns The note's stacking order; higher is nearer front. */ + getZIndex(): number { + return this.getNoteState().zIndex; + } + + /** + * Sets the note's stacking order. + * @param zIndex The new stacking order. + */ + setZIndex(zIndex: number): void { + this.changeNoteProperty_('zIndex', Number(zIndex) || 0); + } + + /** @returns A copy of the note's metadata. */ + getMeta(): NoteMeta { + return {...this.getNoteState().meta}; + } + + /** + * Replaces the note's metadata wholesale, without firing an event or + * bumping `updatedAt`. Used when loading, so a round-trip preserves + * timestamps verbatim. + * @param meta The metadata to restore. + */ + restoreMeta(meta: Partial): void { + this.getNoteState().meta = {...this.getNoteState().meta, ...meta}; + } + + /** + * Records a body edit, then hands off to core. + * + * The note's own setters all touch the metadata through + * `changeNoteProperty_`, but the body is core's and goes nowhere near it - + * so without this, `updatedAt` would track the title, colour, pin and + * stacking order while ignoring the thing people actually spend their time + * changing, and a note edited all afternoon would still claim it was last + * touched when it was named. + * + * No event of our own: core already fires its own change event for the + * text, and the timestamp rides along in the note's state. + * + * @param text The new body text. + */ + setText(text: string): void { + if (text !== this.getText()) this.touchMeta(); + super.setText(text); + } + + /** Records that the note changed just now. */ + touchMeta(): void { + this.getNoteState().meta.updatedAt = new Date().toISOString(); + } + + /** + * Returns a plain, JSON-serializable copy of every note-specific field. + * @returns The note's extra state. + */ + saveNoteState(): NoteState { + const state = this.getNoteState(); + return { + title: state.title, + colour: state.colour, + pinned: state.pinned, + locked: state.locked, + zIndex: state.zIndex, + meta: {...state.meta}, + }; + } + + /** + * Applies a property change without firing an event. + * + * This is the single write path: `changeNoteProperty_` uses it for user + * edits, and {@link NoteChange} uses it to replay undo and redo. + * + * @param property The property name, or '*' for a whole state + * object. + * @param value The value to apply. + */ + applyNoteProperty(property: NoteProperty, value?: NotePropertyValue): void { + const state = this.getNoteState(); + // The property name is what says which shape `value` has — a + // correspondence the type system cannot see across two independent + // parameters, so each case asserts the one it knows it has. + switch (property) { + case 'title': + state.title = value as string; + this.renderTitle(); + break; + case 'colour': + state.colour = value as string; + this.renderColour(); + break; + case 'pinned': + state.pinned = value as boolean; + this.applyPinned(); + break; + case 'locked': + state.locked = value as boolean; + this.applyLocked(); + break; + case 'zIndex': + state.zIndex = value as number; + this.applyZIndex(); + break; + case 'meta': + state.meta = {...(value as NoteMeta)}; + break; + case '*': { + // Null is the "forward" half of a delete snapshot: there is nothing + // to restore when replaying towards the deletion. + if (!value) break; + const whole = value as NoteState; + state.title = whole.title; + state.colour = whole.colour; + state.pinned = whole.pinned; + // Coerced, unlike its neighbours: '*' also replays a NoteChange + // serialized before locking existed, where the key is simply absent. + // Left undefined it would survive into `saveNoteState`, and the + // JSON.stringify comparison in `changeNoteProperty_` would then treat + // a real lock as no change at all. + state.locked = !!whole.locked; + state.zIndex = whole.zIndex; + state.meta = {...whole.meta}; + this.renderTitle(); + this.renderColour(); + // Before `applyPinned`, which ends by laying the title row out again: + // that pass should see both markers' final state, not one of them + // mid-flight. + this.applyLocked(); + this.applyPinned(); + this.applyZIndex(); + break; + } + default: + console.warn(`Unknown note property: ${property}`); + } + } + + /** + * Reads a single note property. + * @param property The property name, or '*'. + * @returns The current value. + */ + protected readNoteProperty_(property: NoteProperty): NotePropertyValue { + return property === '*' + ? this.saveNoteState() + : this.getNoteState()[property]; + } + + /** + * Applies a change and fires an undoable event describing it. + * @param property The property name. + * @param value The new value. + */ + protected changeNoteProperty_( + property: NoteProperty, + value: NotePropertyValue, + ): void { + const oldValue = this.readNoteProperty_(property); + if (JSON.stringify(oldValue) === JSON.stringify(value)) return; + + this.applyNoteProperty(property, value); + this.touchMeta(); + + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new NoteChange(this, property, oldValue, value)); + } + } + + /** Reflects the title in the DOM. Overridden by the rendered subclass. */ + renderTitle(): void {} + + /** Reflects the colour in the DOM. Overridden by the rendered subclass. */ + renderColour(): void {} + + /** Applies the pinned flag. Holding a note in place works headlessly. */ + applyPinned(): void { + this.setMovable(!this.getNoteState().pinned); + } + + /** + * Applies the locked flag, which is what actually makes a note read-only. + * + * Locking owns `editable` and `deletable`; pinning owns `movable`. Neither + * ever writes the other's flag, and that is the whole reason the two + * compose: if both drove `movable`, unpinning a locked note would quietly + * hand its movement back. + * + * Unlocking sets both flags to true unconditionally, so a host that had + * independently called `setEditable(false)` loses that when a note it + * locked is unlocked. Locking owns those two flags outright; a host that + * needs its own read-only state should not also drive them. + */ + applyLocked(): void { + const locked = this.getNoteState().locked; + this.setEditable(!locked); + this.setDeletable(!locked); + } + + /** Applies the stacking order. Overridden by the rendered subclass. */ + applyZIndex(): void {} + + /** + * Raises this note above its neighbours. + * + * A no-op here: raising something means moving it in the DOM, and a + * headless note has none. `restackNotes` calls this on every note it + * sorts, so the seam is what lets it work without knowing which kind it + * has in front of it. + */ + bringToFront(): void {} + + /** + * Disposes of the note. + * + * A snapshot event is fired *before* the delete so that undoing a deletion + * restores the extra fields: a group is undone in reverse, so core's + * `CommentCreate` rebuilds the bare note first and this event then + * repaints it. Core's delete event only carries the fields its own + * serializer knows about. + * + * Both events must share a group, or undo would stop after the first and + * the user would need a second undo to get the colour back. + */ + dispose(): void { + asOneUndoStep(() => { + if (!this.isDeadOrDying() && Blockly.Events.isEnabled()) { + // newValue is null rather than the state: an event whose two sides + // are equal is dropped by isNull() and never reaches the undo stack. + Blockly.Events.fire( + new NoteChange(this, '*', this.saveNoteState(), null), + ); + } + super.dispose(); + }); + } + }; + +/** + * The mixin applied to the headless comment, with its real constructor. + * + * Naming the result is what makes both of the following work: the constructor + * parameters survive (a mixin's base is `...args: any[]`, which would otherwise + * erase them), and `declaration: true` has a type it can write down instead of + * an anonymous class expression. + */ +export const NoteCommentBase = NoteMixin( + Blockly.comments.WorkspaceComment, +) as new ( + workspace: Blockly.Workspace, + id?: string, +) => Blockly.comments.WorkspaceComment & NoteSurface; + +/** + * The mixin applied to the rendered comment. See `NoteCommentBase`. + */ +export const RenderedNoteBase = NoteMixin( + Blockly.comments.RenderedWorkspaceComment, +) as new ( + workspace: Blockly.WorkspaceSvg, + id?: string, +) => Blockly.comments.RenderedWorkspaceComment & NoteSurface; diff --git a/blockly-workspace-notes/src/model/stacking.ts b/blockly-workspace-notes/src/model/stacking.ts new file mode 100644 index 0000000..c0d2131 --- /dev/null +++ b/blockly-workspace-notes/src/model/stacking.ts @@ -0,0 +1,60 @@ +/** + * @fileoverview Reading and writing the order notes stack in. + * + * A note's z-index is a number it stores; the browser honours DOM order + * instead. `restackNotes` is what reconciles the two, and the two lookups + * below are what a caller uses to put a note in front of, or behind, + * everything already on the workspace. + * + * Nothing here imports from `model/`, deliberately. `Note.applyZIndex` calls + * `restackNotes`, so an import in the other direction would be a cycle; the + * three things this module needs from a note - recognising one, reading its + * z-index, and raising it - are all reached without naming the class, through + * `isNote` and the `bringToFront` seam. + */ + +import * as Blockly from 'blockly/core'; + +import {isNote} from '../utils/guards'; + +/** + * Reorders the notes on a workspace so their DOM order matches their + * z-indices. + * + * @param workspace The workspace to restack. + */ +export function restackNotes(workspace: Blockly.Workspace): void { + if (!workspace.rendered) return; + // Two passes rather than one predicate: combining them would need the + // narrowed type spelled out, which means naming the class again. + workspace + .getTopComments(false) + .filter(isNote) + .filter((note) => !note.isDeadOrDying()) + .sort((a, b) => a.getZIndex() - b.getZIndex()) + .forEach((note) => note.bringToFront()); +} + +/** + * @param workspace The workspace to inspect. + * @returns One more than the highest z-index in use. + */ +export function nextZIndex(workspace: Blockly.Workspace): number { + const zIndices = workspace + .getTopComments(false) + .filter(isNote) + .map((note) => note.getZIndex()); + return zIndices.length ? Math.max(...zIndices) + 1 : 1; +} + +/** + * @param workspace The workspace to inspect. + * @returns One less than the lowest z-index in use. + */ +export function previousZIndex(workspace: Blockly.Workspace): number { + const zIndices = workspace + .getTopComments(false) + .filter(isNote) + .map((note) => note.getZIndex()); + return zIndices.length ? Math.min(...zIndices) - 1 : -1; +} diff --git a/blockly-workspace-notes/src/plugin.ts b/blockly-workspace-notes/src/plugin.ts new file mode 100644 index 0000000..1dec9d2 --- /dev/null +++ b/blockly-workspace-notes/src/plugin.ts @@ -0,0 +1,201 @@ +/** + * @fileoverview `WorkspaceNotes`: the object a host application constructs. + * + * Everything else in the plugin is a piece this class switches on. `init` + * claims the registries — the serializers, the paster, the event, the context + * menu, the `Blockly.Xml` entry points — and `dispose` hands every one of them + * back, so a host can attach notes to a workspace and detach them again + * without leaving Blockly changed. + * + * Importing `ui/css` here is what registers the stylesheet, and it has to + * happen before `Blockly.inject`: `Blockly.Css.register` only affects + * injections that come after it. + */ + +import * as Blockly from 'blockly/core'; + +import './ui/css'; +import {DEFAULT_PALETTE} from './constants/colours'; +import {DEFAULT_SIZE} from './constants/layout'; +import { + applyDefaultNoteSize, + restoreDefaultNoteSize, +} from './model/default_size'; +import {Note} from './model/note'; +import {NoteComment} from './model/note_comment'; +import {nextZIndex} from './model/stacking'; +import { + registerNotePaster, + unregisterNotePaster, +} from './clipboard/note_paster'; +import {registerNoteChangeEvent} from './events/registry'; +import { + registerNoteSerializers, + unregisterNoteSerializers, +} from './serialization/registry'; +import {createNote as makeNote} from './serialization/state'; +import {clearLockPermission, setLockPermission} from './ui/lock_permission'; +import { + registerXmlSupport, + unregisterXmlSupport, +} from './serialization/xml/patch'; +import type {SavedNote} from './types/serialization'; +import type {WorkspaceNotesOptions} from './types/options'; +import { + registerNoteContextMenu, + unregisterNoteContextMenu, +} from './ui/context_menu'; +import {isNote} from './utils/guards'; +import {asOneUndoStep} from './utils/undo'; + +/** + * Adds note support to a workspace. + */ +export class WorkspaceNotes { + /** The workspace this instance is attached to. */ + protected workspace: Blockly.Workspace; + + /** The options this instance was constructed with, over the defaults. */ + protected options: Required; + + /** The workspace's own `newComment`, while ours is in its place. */ + private originalNewComment_: + ((id?: string) => Blockly.comments.WorkspaceComment) | null = null; + + /** Whether `init` has run, so it stays idempotent. */ + private initialized_ = false; + + /** + * @param workspace The workspace to add notes to. A headless workspace works + * too; it simply gets unrendered notes. + * @param options Plugin options. `skipSerializerRegistration` leaves + * persistence entirely to the host app; `emitLegacyComments` keeps + * writing the old `workspaceComments` key as well, which duplicates + * every note and is off by default; `xmlSupport` wraps the `Blockly.Xml` + * entry points so notes survive the older XML format too, and can be + * turned off by a host that only uses JSON. + */ + constructor( + workspace: Blockly.Workspace, + options: WorkspaceNotesOptions = {}, + ) { + this.workspace = workspace; + this.options = { + palette: DEFAULT_PALETTE, + defaultSize: DEFAULT_SIZE, + contextMenu: true, + skipSerializerRegistration: false, + emitLegacyComments: false, + xmlSupport: true, + getAuthor: () => '', + canToggleLock: () => true, + ...options, + }; + } + + /** + * Starts the plugin. + */ + init(): void { + if (this.initialized_) return; + this.initialized_ = true; + + applyDefaultNoteSize(this.options.defaultSize); + + // Patched on the instance rather than the prototype: scoped to this + // workspace and trivially reversible. This is what makes undoing a delete + // rebuild a Note — core's CommentCreate replays through + // `workspace.newComment()`. + this.originalNewComment_ = this.workspace.newComment; + this.workspace.newComment = (id) => makeNote(this.workspace, id); + + registerNoteChangeEvent(); + + // Outside the contextMenu guard below: who may unlock a note is a fact + // about the workspace, not about whether this plugin drew the menu that + // asks. A host driving its own menu still needs to be able to ask. + setLockPermission(this.workspace, this.options.canToggleLock); + + if (!this.options.skipSerializerRegistration) { + registerNoteSerializers({ + emitLegacyComments: this.options.emitLegacyComments, + }); + } + + registerNotePaster(); + + if (this.options.xmlSupport) { + registerXmlSupport(); + } + + if (this.options.contextMenu) { + registerNoteContextMenu({palette: this.options.palette}); + } + } + + /** + * Stops the plugin and restores everything it replaced. + * + * The serializer, paster and context menu registries are global singletons + * shared by every workspace, so they are reference-counted and only really + * restored once the last plugin instance is disposed. + */ + dispose(): void { + if (!this.initialized_) return; + this.initialized_ = false; + + if (this.originalNewComment_) { + this.workspace.newComment = this.originalNewComment_; + this.originalNewComment_ = null; + } + + restoreDefaultNoteSize(); + + if (!this.options.skipSerializerRegistration) { + unregisterNoteSerializers(); + } + + unregisterNotePaster(); + + if (this.options.xmlSupport) { + unregisterXmlSupport(); + } + + clearLockPermission(this.workspace); + + if (this.options.contextMenu) { + unregisterNoteContextMenu(); + } + } + + /** + * Creates a note on the workspace. + * + * @param state Initial values for the note. + * @returns The new note. + */ + createNote(state: Partial = {}): Note | NoteComment { + return asOneUndoStep(() => { + const note = makeNote(this.workspace); + if (state.text) note.setText(state.text); + if (state.title) note.setTitle(state.title); + if (state.colour) note.setColour(state.colour); + if (state.x !== undefined || state.y !== undefined) { + note.moveTo(new Blockly.utils.Coordinate(state.x ?? 0, state.y ?? 0)); + } + note.setZIndex(nextZIndex(this.workspace)); + // After the z-index, which pinning would otherwise raise a second time. + if (state.pinned) note.setPinned(true); + if (state.locked) note.setLocked(true); + note.restoreMeta({author: this.options.getAuthor()}); + return note; + }); + } + + /** + * @returns Every note on the workspace. + */ + getNotes(): Array { + return this.workspace.getTopComments(false).filter(isNote); + } +} diff --git a/blockly-workspace-notes/src/serialization/legacy_comment_adapter.ts b/blockly-workspace-notes/src/serialization/legacy_comment_adapter.ts new file mode 100644 index 0000000..d3b9ac1 --- /dev/null +++ b/blockly-workspace-notes/src/serialization/legacy_comment_adapter.ts @@ -0,0 +1,80 @@ +/** + * @fileoverview A stand-in for Blockly's built-in comment serializer. + * + * A note is a `RenderedWorkspaceComment`, so it also lands in + * `workspace.getTopComments()` and core's own comment serializer would save it + * a second time — reloading would then produce two notes for every one. This + * adapter replaces that serializer: it writes nothing, and exists only so that + * files carrying the old `workspaceComments` key still load, as notes. + */ + +import * as Blockly from 'blockly/core'; + +import type {WorkspaceNotesOptions} from '../types/options'; +import type {SavedNote} from '../types/serialization'; +import {isNote} from '../utils/guards'; +import {appendNote, saveNote} from './state'; + +/** + * Stands in for Blockly's built-in comment serializer. + * + * Writes nothing — notes are the single source of truth — but still reads + * `workspaceComments`, so files saved before this plugin (or by plain Blockly) + * load as notes. + * + * @implements {Blockly.serialization.ISerializer} + */ +export class LegacyCommentAdapter implements Blockly.serialization.ISerializer { + /** + * Whether to keep writing the old workspaceComments key. Off by default, + * since it duplicates every note. + */ + private emitLegacyComments_: boolean; + + /** Runs alongside core's own comment serializer. */ + priority: number; + + /** + * @param options Whether to keep emitting legacy comments. + * @param options.emitLegacyComments Whether to write the old key too. + */ + constructor({emitLegacyComments = false}: WorkspaceNotesOptions = {}) { + this.priority = Blockly.serialization.priorities.WORKSPACE_COMMENTS; + this.emitLegacyComments_ = emitLegacyComments; + } + + /** + * @param workspace The workspace to serialize. + * @returns Legacy comment states, or null. + */ + save(workspace: Blockly.Workspace): SavedNote[] | null { + if (!this.emitLegacyComments_) return null; + const comments = workspace + .getTopComments(false) + .filter(isNote) + .map((note) => saveNote(note, {addCoordinates: true, saveIds: true})); + return comments.length ? comments : null; + } + + /** + * @param state Legacy comment states. + * @param workspace The workspace to load into. + */ + load(state: object, workspace: Blockly.Workspace): void { + const recordUndo = Blockly.Events.getRecordUndo(); + // The interface types this as `object`; core passes the array core wrote. + for (const commentState of state as SavedNote[]) { + // Notes load first (higher priority). If a file somehow carries both + // keys, the note wins rather than being duplicated under a fresh ID. + if (commentState.id && workspace.getCommentById(commentState.id)) { + continue; + } + appendNote(commentState, workspace, {recordUndo}); + } + } + + /** + * Intentionally empty; NoteSerializer.clear disposes every comment. + */ + clear(): void {} +} diff --git a/blockly-workspace-notes/src/serialization/migrations.ts b/blockly-workspace-notes/src/serialization/migrations.ts new file mode 100644 index 0000000..f4523c0 --- /dev/null +++ b/blockly-workspace-notes/src/serialization/migrations.ts @@ -0,0 +1,65 @@ +/** + * @fileoverview Upgrading a `workspaceNotes` payload written by an older + * version of this plugin. + * + * Every save file ever written has to keep loading, so this is the one place + * that knows what earlier versions looked like. A change that an older file + * cannot be read through — a renamed or retyped field, a moved one, or a new + * required one — needs a `SCHEMA_VERSION` bump in `constants/serialization.ts` + * and an entry in `MIGRATIONS` below, keyed by the version it upgrades *from*. + * + * A new *optional* field needs neither. Its absence is already its default, so + * the migration would be the identity function — and bumping is not free: + * `migrate` warns whenever a file's version runs ahead of the plugin reading + * it, so every file written by the new build would make an older deployed + * build complain about a field it was never going to honour anyway. + */ + +import {SCHEMA_VERSION} from '../constants/serialization'; +import type {NotesPayload, SavedNote} from '../types/serialization'; + +/** + * Upgrades an older payload to the current schema. + * + * Keyed by the version being upgraded *from*; each entry returns a payload at + * the next version. + */ +const MIGRATIONS: Record NotesPayload> = { + // v0 is the unversioned shape: a bare array of note states, which is also + // what a legacy `workspaceComments` list looks like. + 0: (state) => ({version: 1, notes: state.notes ?? []}), +}; + +/** + * Normalizes and upgrades a `workspaceNotes` payload. + * + * @param state The raw payload. + * @returns A payload at the current schema version. + */ +export function migrate( + state: NotesPayload | SavedNote[] | undefined, +): NotesPayload { + let current: NotesPayload = Array.isArray(state) + ? {version: 0, notes: state} + : {version: Number(state?.version) || 0, notes: state?.notes ?? []}; + + if (current.version > SCHEMA_VERSION) { + console.warn( + `Loading workspaceNotes v${current.version} with a plugin that ` + + `understands v${SCHEMA_VERSION}; unknown fields will be dropped.`, + ); + return {version: SCHEMA_VERSION, notes: current.notes}; + } + + while (current.version < SCHEMA_VERSION) { + const step = MIGRATIONS[current.version]; + if (!step) break; + const next = step(current); + // Guard against a migration that fails to advance the version, which + // would otherwise spin forever. + if ((Number(next.version) || 0) <= current.version) break; + current = next; + } + + return current; +} diff --git a/blockly-workspace-notes/src/serialization/note_serializer.ts b/blockly-workspace-notes/src/serialization/note_serializer.ts new file mode 100644 index 0000000..21e6123 --- /dev/null +++ b/blockly-workspace-notes/src/serialization/note_serializer.ts @@ -0,0 +1,79 @@ +/** + * @fileoverview The serializer that owns the `workspaceNotes` key. + * + * Notes ride along in the same payload as blocks and variables: + * + * { + * "blocks": {...}, + * "workspaceNotes": {"version": 1, "notes": [...]} + * } + * + * It also owns clearing the workspace for both keys — see `clear` — which is + * why `LegacyCommentAdapter.clear` is deliberately empty. + */ + +import * as Blockly from 'blockly/core'; + +import {SCHEMA_VERSION} from '../constants/serialization'; +import {restackNotes} from '../model/stacking'; +import type {NotesPayload} from '../types/serialization'; +import {isNote} from '../utils/guards'; +import {migrate} from './migrations'; +import {appendNote, saveNote} from './state'; + +/** + * Saves and loads notes under the `workspaceNotes` key. + * + * @implements {Blockly.serialization.ISerializer} + */ +export class NoteSerializer implements Blockly.serialization.ISerializer { + /** + * Ordered just above core's comment priority so that notes load before the + * legacy adapter runs and it can skip anything already present. Equal + * priorities are ordered arbitrarily relative to each other, so the two must + * differ. + */ + priority = Blockly.serialization.priorities.WORKSPACE_COMMENTS + 1; + + /** + * @param workspace The workspace to serialize. + * @returns The notes payload, or null when there are none. + */ + save(workspace: Blockly.Workspace): NotesPayload | null { + const notes = workspace + .getTopComments(false) + .filter(isNote) + .map((note) => saveNote(note, {addCoordinates: true, saveIds: true})); + // Returning null omits the key entirely; an empty object would be falsy + // to `workspaces.load` anyway and is never worth writing. + return notes.length ? {version: SCHEMA_VERSION, notes} : null; + } + + /** + * @param state The notes payload. + * @param workspace The workspace to load into. + */ + load(state: object, workspace: Blockly.Workspace): void { + const recordUndo = Blockly.Events.getRecordUndo(); + for (const noteState of migrate(state as NotesPayload).notes) { + appendNote(noteState, workspace, {recordUndo}); + } + restackNotes(workspace); + } + + /** + * Disposes of every comment on the workspace. + * + * This owns clearing for both keys: the legacy adapter deliberately does + * nothing so that nothing is disposed twice. Non-note comments are included, + * matching what core's serializer did before we replaced it — otherwise they + * would survive a load and accumulate. + * + * @param workspace The workspace to clear. + */ + clear(workspace: Blockly.Workspace): void { + for (const comment of workspace.getTopComments(false)) { + comment.dispose(); + } + } +} diff --git a/blockly-workspace-notes/src/serialization/registry.ts b/blockly-workspace-notes/src/serialization/registry.ts new file mode 100644 index 0000000..4ea88a3 --- /dev/null +++ b/blockly-workspace-notes/src/serialization/registry.ts @@ -0,0 +1,77 @@ +/** + * @fileoverview Swapping the plugin's serializers into Blockly's registry, and + * putting core's back. + * + * The registry is a global singleton while the plugin is per-workspace, so + * registration is reference-counted: two workspaces on one page register once + * between them, and core's serializer only returns when the last one goes. + */ + +import * as Blockly from 'blockly/core'; + +import { + COMMENT_SERIALIZER_NAME, + NOTE_SERIALIZER_NAME, +} from '../constants/serialization'; +import type {WorkspaceNotesOptions} from '../types/options'; +import {LegacyCommentAdapter} from './legacy_comment_adapter'; +import {NoteSerializer} from './note_serializer'; + +/** + * Whether this module has swapped the serializers in yet. The registry is a + * global singleton but the plugin is per-workspace, so registration is + * reference-counted. + */ +let registrationCount = 0; + +/** + * The comment serializer displaced on first registration, so it can be put + * back exactly as it was. + */ +let displacedCommentSerializer: Blockly.serialization.ISerializer | null = null; + +/** + * Registers the note serializer and replaces Blockly's comment serializer. + * + * @param [options] Serializer options, + * honoured on the first registration. + */ +export function registerNoteSerializers( + options: WorkspaceNotesOptions = {}, +): void { + if (registrationCount++) return; + + displacedCommentSerializer = Blockly.registry.getObject( + Blockly.registry.Type.SERIALIZER, + COMMENT_SERIALIZER_NAME, + false, + ); + + // `register` throws on a duplicate name, so the built-in must go first. + Blockly.serialization.registry.unregister(COMMENT_SERIALIZER_NAME); + Blockly.serialization.registry.register( + COMMENT_SERIALIZER_NAME, + new LegacyCommentAdapter(options), + ); + Blockly.serialization.registry.register( + NOTE_SERIALIZER_NAME, + new NoteSerializer(), + ); +} + +/** + * Restores Blockly's built-in comment serializer. + */ +export function unregisterNoteSerializers(): void { + if (--registrationCount > 0) return; + registrationCount = 0; + + Blockly.serialization.registry.unregister(NOTE_SERIALIZER_NAME); + Blockly.serialization.registry.unregister(COMMENT_SERIALIZER_NAME); + Blockly.serialization.registry.register( + COMMENT_SERIALIZER_NAME, + displacedCommentSerializer ?? + new Blockly.serialization.workspaceComments.WorkspaceCommentSerializer(), + ); + displacedCommentSerializer = null; +} diff --git a/blockly-workspace-notes/src/serialization/state.ts b/blockly-workspace-notes/src/serialization/state.ts new file mode 100644 index 0000000..67e3bd9 --- /dev/null +++ b/blockly-workspace-notes/src/serialization/state.ts @@ -0,0 +1,189 @@ +/** + * @fileoverview Turning a note into plain JSON and back. + * + * These three functions are the whole of the note format. Everything else in + * `serialization/` is about *when* they run: the serializers call them for a + * whole workspace, the XML layer calls them for one note at a time, and the + * paster calls `appendNote` to rebuild a copied note. + * + * `saveNote` is sparse by design, matching core's comment serializer — width + * and height always, everything else only when it differs from the default — + * so saved files stay small and diffable. + */ + +import * as Blockly from 'blockly/core'; + +import {DEFAULT_COLOUR} from '../constants/colours'; +import {Note} from '../model/note'; +import {NoteComment} from '../model/note_comment'; +import type {SavedNote} from '../types/serialization'; + +/** + * Constructs the right note class for a workspace. A headless workspace + * cannot hold a rendered note, and that is the only kind a Node test can make. + * + * @param workspace The workspace to create the note on. + * @param [id] An optional ID. + * @returns The new note. + */ +export function createNote( + workspace: Blockly.Workspace, + id?: string, +): Note | NoteComment { + // `rendered` is what distinguishes the two at runtime; the compiler cannot + // narrow a base Workspace from a boolean flag. + return workspace.rendered + ? new Note(workspace as Blockly.WorkspaceSvg, id) + : new NoteComment(workspace, id); +} + +/** + * Serializes a single note. + * + * Sparse by design, matching core's comment serializer: width and height are + * always written, and everything else only when it differs from the default. + * That keeps saved files small and diffable. + * + * @param note The note to save. + * @param options What to include beyond the note's own fields. + * @param options.addCoordinates Whether to write the note's position. + * @param options.saveIds Whether to write the note's id. + * @returns The note's JSON state. + */ +export function saveNote( + note: Note | NoteComment, + {addCoordinates = false, saveIds = true} = {}, +): SavedNote { + const workspace = note.workspace; + // Null-prototype, as core's own serializer builds it: the state is handed + // straight to JSON, and nothing inherited belongs in it. + const state: SavedNote = Object.create(null); + + state.height = note.getSize().height; + state.width = note.getSize().width; + + if (saveIds) state.id = note.id; + + if (addCoordinates) { + const loc = note.getRelativeToSurfaceXY(); + state.x = workspace.RTL ? workspace.getWidth() - loc.x : loc.x; + state.y = loc.y; + } + + if (note.getText()) state.text = note.getText(); + if (note.isCollapsed()) state.collapsed = true; + + // Read here rather than below with the rest of the note's own fields, + // because the three core flags underneath them are what these two imply. + // A plain comment made outside the plugin has neither accessor. + const locked = typeof note.isLocked === 'function' && note.isLocked(); + const pinned = typeof note.isPinned === 'function' && note.isPinned(); + + // `isOwn*` rather than `is*`: a read-only *workspace* must not poison the + // per-note flags we persist. A locked note is read-only and undeletable by + // definition, and a pinned one immovable, so writing those out as well would + // be noise - and would make a file say twice what it means once. + if (!note.isOwnEditable() && !locked) state.editable = false; + if (!note.isOwnDeletable() && !locked) state.deletable = false; + if (!note.isOwnMovable() && !pinned) state.movable = false; + + // Note-specific fields. A plain comment created outside the plugin has + // none of these accessors; it simply serializes without them. + if (typeof note.getTitle !== 'function') return state; + + if (note.getTitle()) state.title = note.getTitle(); + // Case-insensitive: `setColour` normalizes through Blockly's parser, but a + // caller could have written an uppercase hex straight into the state. + if (note.getColour().toLowerCase() !== DEFAULT_COLOUR) { + state.colour = note.getColour(); + } + if (pinned) state.pinned = true; + if (locked) state.locked = true; + if (note.getZIndex()) state.zIndex = note.getZIndex(); + + const meta = note.getMeta(); + if (Object.values(meta).some((value) => value)) state.meta = meta; + + return state; +} + +/** + * Creates a note on a workspace from its JSON state. + * + * @param state The note state to load. + * @param workspace The workspace to add the note to. + * @param [options] Whether the resulting events + * should be undoable. + * @param options.recordUndo Whether the append should be undoable. + * @returns The created note. + */ +export function appendNote( + state: SavedNote, + workspace: Blockly.Workspace, + {recordUndo = false} = {}, +): Note | NoteComment { + const previousRecordUndo = Blockly.Events.getRecordUndo(); + Blockly.Events.setRecordUndo(recordUndo); + const existingGroup = Blockly.Events.getGroup(); + if (!existingGroup) Blockly.Events.setGroup(true); + + let note; + try { + note = createNote(workspace, state.id); + + if (state.text !== undefined) note.setText(state.text); + + if (state.x !== undefined || state.y !== undefined) { + // A missing coordinate keeps the one the note already has, rather than + // snapping it to the origin. The RTL flip applies only to a value that + // came out of the file — `getRelativeToSurfaceXY` is already in + // workspace coordinates, so flipping it would move the note. + const defaultLoc = note.getRelativeToSurfaceXY(); + const x = + state.x === undefined + ? defaultLoc.x + : workspace.RTL + ? workspace.getWidth() - state.x + : state.x; + note.moveTo(new Blockly.utils.Coordinate(x, state.y ?? defaultLoc.y)); + } + + if (state.width !== undefined || state.height !== undefined) { + // Likewise: half a size is not a reason to collapse the other half to + // zero, which would leave a note that is saved but invisible. + const defaultSize = note.getSize(); + note.setSize( + new Blockly.utils.Size( + state.width ?? defaultSize.width, + state.height ?? defaultSize.height, + ), + ); + } + + if (state.collapsed !== undefined) note.setCollapsed(state.collapsed); + if (state.editable !== undefined) note.setEditable(state.editable); + if (state.movable !== undefined) note.setMovable(state.movable); + if (state.deletable !== undefined) note.setDeletable(state.deletable); + + if (typeof note.setTitle === 'function') { + if (state.colour !== undefined) note.setColour(state.colour); + if (state.title !== undefined) note.setTitle(state.title); + if (state.zIndex !== undefined) note.setZIndex(state.zIndex); + // Applied after `movable`, which it overrides. + if (state.pinned) note.setPinned(true); + // Likewise for `editable` and `deletable`. Truthy-gated rather than + // tested against undefined, unlike the fields above: `setLocked(false)` + // would restore both flags and undo an explicit `editable: false` + // applied a few lines up, and false is the default in any case. + if (state.locked) note.setLocked(true); + // Last, and deliberately not through a setter: restoring metadata must + // not stamp a fresh `updatedAt` over the one we just loaded. + if (state.meta) note.restoreMeta(state.meta); + } + } finally { + Blockly.Events.setGroup(existingGroup); + Blockly.Events.setRecordUndo(previousRecordUndo); + } + + return note; +} diff --git a/blockly-workspace-notes/src/serialization/xml/dom.ts b/blockly-workspace-notes/src/serialization/xml/dom.ts new file mode 100644 index 0000000..e8d89dd --- /dev/null +++ b/blockly-workspace-notes/src/serialization/xml/dom.ts @@ -0,0 +1,177 @@ +/** + * @fileoverview Reading and writing one `` element. + * + * Blockly 13 still writes and reads workspace comments as XML, and a note has + * to survive that round trip. Everything here funnels through the same state + * objects `serialization/state.ts` produces, so the XML and JSON formats + * cannot drift apart. + * + * The extra attributes are all optional, so an element written here still + * reads as an ordinary comment in plain Blockly — it simply ignores what it + * does not recognise. + */ + +import * as Blockly from 'blockly/core'; + +import type {Note} from '../../model/note'; +import type {NoteComment} from '../../model/note_comment'; +import type {SavedNote} from '../../types/serialization'; +import {appendNote, saveNote} from '../state'; + +/** + * Converts a note's JSON state into attributes on a `` element. + * + * Only the note-specific fields are written; core has already supplied + * `id`/`x`/`y`/`w`/`h` and the text content. Everything added here is + * optional, so older Blockly reads the element as an ordinary comment and + * simply ignores what it does not recognise. + * + * @param elem The `` element to decorate. + * @param state The note state, as produced by `saveNote`. + */ +export function decorateElement(elem: Element, state: SavedNote): void { + if (state.title) elem.setAttribute('title', state.title); + if (state.colour) elem.setAttribute('colour', state.colour); + if (state.pinned) elem.setAttribute('pinned', 'true'); + if (state.locked) elem.setAttribute('locked', 'true'); + // `z` rather than `zIndex`, grouping it with core's terse x/y/w/h geometry. + if (state.zIndex) elem.setAttribute('z', `${state.zIndex}`); + if (state.meta?.author) elem.setAttribute('author', state.meta.author); + if (state.meta?.createdAt) elem.setAttribute('created', state.meta.createdAt); + if (state.meta?.updatedAt) elem.setAttribute('updated', state.meta.updatedAt); +} + +/** + * Reads a `` element into the state shape the JSON loader uses. + * + * Mirrors core's `loadWorkspaceComment` for the shared attributes, including + * its `isNaN` guards, so a malformed value is skipped rather than written as + * NaN. The RTL flip is left to `appendNote`, which already applies it. + * + * @param elem A `` element. + * @returns The note state. + */ +export function domToNoteState(elem: Element): SavedNote { + const state: SavedNote = Object.create(null); + + const id = elem.getAttribute('id'); + if (id) state.id = id; + + const x = parseInt(elem.getAttribute('x') ?? '', 10); + const y = parseInt(elem.getAttribute('y') ?? '', 10); + if (!isNaN(x) && !isNaN(y)) { + state.x = x; + state.y = y; + } + + const width = parseInt(elem.getAttribute('w') ?? '', 10); + const height = parseInt(elem.getAttribute('h') ?? '', 10); + if (!isNaN(width) && !isNaN(height)) { + state.width = width; + state.height = height; + } + + if (elem.textContent) state.text = elem.textContent; + if (elem.getAttribute('collapsed') === 'true') state.collapsed = true; + if (elem.getAttribute('editable') === 'false') state.editable = false; + if (elem.getAttribute('movable') === 'false') state.movable = false; + if (elem.getAttribute('deletable') === 'false') state.deletable = false; + + const title = elem.getAttribute('title'); + if (title) state.title = title; + + const colour = elem.getAttribute('colour'); + if (colour) state.colour = colour; + + if (elem.getAttribute('pinned') === 'true') state.pinned = true; + if (elem.getAttribute('locked') === 'true') state.locked = true; + + const zIndex = parseInt(elem.getAttribute('z') ?? '', 10); + if (!isNaN(zIndex)) state.zIndex = zIndex; + + const meta = { + author: elem.getAttribute('author') ?? '', + createdAt: elem.getAttribute('created') ?? '', + updatedAt: elem.getAttribute('updated') ?? '', + }; + if (Object.values(meta).some((value) => value)) state.meta = meta; + + return state; +} + +/** + * Serializes a note to a `` element. + * + * @param note The note to save. + * @param [skipId] True to omit the note's ID. + * @returns The `` element. + */ +export function noteToDom(note: Note | NoteComment, skipId = false): Element { + const elem = Blockly.utils.xml.createElement('comment'); + const state = saveNote(note, {addCoordinates: true, saveIds: !skipId}); + + if (state.id) elem.setAttribute('id', state.id); + elem.setAttribute('x', `${state.x}`); + elem.setAttribute('y', `${state.y}`); + elem.setAttribute('w', `${state.width}`); + elem.setAttribute('h', `${state.height}`); + + if (state.text) elem.textContent = state.text; + if (state.collapsed) elem.setAttribute('collapsed', 'true'); + if (state.editable === false) elem.setAttribute('editable', 'false'); + if (state.movable === false) elem.setAttribute('movable', 'false'); + if (state.deletable === false) elem.setAttribute('deletable', 'false'); + + decorateElement(elem, state); + return elem; +} + +/** + * Loads a `` element as a note. + * + * @param elem A `` element. + * @param workspace The workspace to load into. + * @returns The created note. + */ +export function domToNote( + elem: Element, + workspace: Blockly.Workspace, +): Note | NoteComment { + return appendNote(domToNoteState(elem), workspace, { + // Core's XML loader does not force recordUndo the way the JSON loader + // does, so honour whatever the caller has set. + recordUndo: Blockly.Events.getRecordUndo(), + }); +} + +/** + * @param xml An `` element. + * @returns Its direct `` children. Block comments + * are nested inside `` and so are untouched. + */ +export function topLevelComments(xml: Element): Element[] { + return Array.from(xml.childNodes).filter( + (node): node is Element => node.nodeName.toLowerCase() === 'comment', + ); +} + +/** + * Returns a copy of an `` element with its top-level comments removed, + * alongside the removed elements. + * + * Loading is delegated to Blockly for everything except comments, and the + * notes are created afterwards. The caller's DOM is cloned rather than + * mutated, since callers commonly reuse the same document. + * + * @param xml An `` element. + * @returns The split. + */ +export function splitComments(xml: Element): { + stripped: Element; + comments: Element[]; +} { + const stripped = xml.cloneNode(true) as Element; + const comments = topLevelComments(stripped); + for (const elem of comments) stripped.removeChild(elem); + return {stripped, comments}; +} diff --git a/blockly-workspace-notes/src/serialization/xml/patch.ts b/blockly-workspace-notes/src/serialization/xml/patch.ts new file mode 100644 index 0000000..fdb99cb --- /dev/null +++ b/blockly-workspace-notes/src/serialization/xml/patch.ts @@ -0,0 +1,174 @@ +/** + * @fileoverview Wrapping the `Blockly.Xml` entry points so notes survive an + * XML round trip. + * + * Without this a note saved through `Blockly.Xml.workspaceToDom` would + * silently lose its title, colour, pinned state and metadata, and would load + * back as a plain comment — `Xml.loadWorkspaceComment` hardcodes + * `new RenderedWorkspaceComment(...)` instead of going through + * `workspace.newComment()`, so the plugin's usual hook does not reach it. + * + * There is no registry for XML the way there is for JSON serializers, so the + * exports on the `Blockly.Xml` namespace are wrapped instead. Each has to be + * wrapped individually: Blockly's own `appendDomToWorkspace` and + * `clearWorkspaceAndLoadFromXml` call `domToWorkspace` through a module-local + * binding, so patching that one export does not reach them. + * + * Like the other registries this touches, `Blockly.Xml` is global while the + * plugin is per-workspace, so the wrappers are reference-counted. + */ + +import * as Blockly from 'blockly/core'; + +import {isNote} from '../../utils/guards'; +import {asOneUndoStep} from '../../utils/undo'; +import {saveNote} from '../state'; +import { + decorateElement, + domToNote, + noteToDom, + splitComments, + topLevelComments, +} from './dom'; + +/** The entry points wrapped on the Blockly.Xml namespace. */ +const PATCHED = [ + 'workspaceToDom', + 'domToWorkspace', + 'appendDomToWorkspace', + 'clearWorkspaceAndLoadFromXml', + 'saveWorkspaceComment', + 'loadWorkspaceComment', +] as const; + +/** One of the six names above. */ +type PatchedName = (typeof PATCHED)[number]; + +/** The six entry points, as a writable record. */ +type PatchedXml = {-readonly [K in PatchedName]: (typeof Blockly.Xml)[K]}; + +/** + * `Blockly.Xml` is an ES module namespace object, so TypeScript treats its + * members as read-only. Replacing them is exactly what this module does — + * there is no registry for XML the way there is for JSON serializers — so the + * namespace is viewed through one mutable alias, declared once, here. Each + * assignment below is still checked against Blockly's real signature. + */ +const Xml = Blockly.Xml as typeof Blockly.Xml & PatchedXml; + +/** The original Blockly.Xml functions, kept so they can be restored. */ +let originals: PatchedXml | null = null; + +/** + * Reference count, since Blockly.Xml is global but the plugin is per + * workspace: the wrappers stay in place until the last instance goes away. + */ +let registrationCount = 0; + +/** + * Wraps the Blockly.Xml entry points so notes survive an XML round trip. + */ +export function registerXmlSupport(): void { + if (registrationCount++) return; + + originals = { + workspaceToDom: Xml.workspaceToDom, + domToWorkspace: Xml.domToWorkspace, + appendDomToWorkspace: Xml.appendDomToWorkspace, + clearWorkspaceAndLoadFromXml: Xml.clearWorkspaceAndLoadFromXml, + saveWorkspaceComment: Xml.saveWorkspaceComment, + loadWorkspaceComment: Xml.loadWorkspaceComment, + }; + // Captured non-null: the wrappers below only exist while this is populated, + // which the module-level `let` cannot express. + const saved = originals; + + /** + * Runs a load through Blockly with comments held back, then adds the notes. + * Wrapped in one event group so a whole XML load is a single undo step; + * Blockly's own loader reuses an open group rather than starting its own. + * + * @param load The original Blockly loader to delegate to. + * @param xml The XML being loaded. + * @param workspace The target workspace. + * @returns The new block IDs, from Blockly. + */ + const loadWithNotes = ( + load: (xml: Element, workspace: W) => string[], + xml: Element, + workspace: W, + ): string[] => { + const {stripped, comments} = splitComments(xml); + return asOneUndoStep(() => { + const blockIds = load.call(Xml, stripped, workspace); + for (const elem of comments) domToNote(elem, workspace); + return blockIds; + }); + }; + + Xml.workspaceToDom = function (workspace, skipId = false) { + const dom = saved.workspaceToDom.call(Xml, workspace, skipId); + // Blockly emits one per top comment, in this same order, so the + // two line up by index. Only extra attributes are added, leaving Blockly's + // own output — including its RTL handling — exactly as it was. + const notes = workspace.getTopComments(); + topLevelComments(dom).forEach((elem, i) => { + const note = notes[i]; + if (isNote(note)) { + decorateElement(elem, saveNote(note, {addCoordinates: true})); + } + }); + return dom; + }; + + Xml.domToWorkspace = function (xml, workspace) { + return loadWithNotes(saved.domToWorkspace, xml, workspace); + }; + + // Blockly's own versions of these two reach domToWorkspace through a + // module-local binding, so the patch above never runs for them. Delegating + // the stripped XML to the originals keeps their extra behaviour — the + // block-offset maths in append, the clear in the other — intact. + Xml.appendDomToWorkspace = function (xml, workspace) { + return loadWithNotes(saved.appendDomToWorkspace, xml, workspace); + }; + + Xml.clearWorkspaceAndLoadFromXml = function (xml, workspace) { + return loadWithNotes(saved.clearWorkspaceAndLoadFromXml, xml, workspace); + }; + + // Direct callers of the per-comment helpers get note support too. + Xml.saveWorkspaceComment = function (comment, skipId = false) { + return isNote(comment) + ? noteToDom(comment, skipId) + : saved.saveWorkspaceComment.call(Xml, comment, skipId); + }; + + Xml.loadWorkspaceComment = function (elem, workspace) { + return domToNote(elem, workspace); + }; +} + +/** + * Puts one saved function back. + * + * Generic so the assignment is `PatchedXml[K] = PatchedXml[K]` for a single + * `K`, which TypeScript accepts; indexing with the whole union would not + * correlate the two sides. + * + * @param name Which entry point to restore. + * @param saved The saved originals. + */ +function restoreOne(name: K, saved: PatchedXml): void { + const target: PatchedXml = Xml; + target[name] = saved[name]; +} + +/** Restores Blockly's own XML functions. */ +export function unregisterXmlSupport(): void { + if (--registrationCount > 0) return; + registrationCount = 0; + if (!originals) return; + for (const name of PATCHED) restoreOne(name, originals); + originals = null; +} diff --git a/blockly-workspace-notes/src/types/clipboard.ts b/blockly-workspace-notes/src/types/clipboard.ts new file mode 100644 index 0000000..19b4146 --- /dev/null +++ b/blockly-workspace-notes/src/types/clipboard.ts @@ -0,0 +1,17 @@ +/** + * @fileoverview What a copied note puts on the clipboard. + */ + +import type {NoteState} from './note'; + +/** + * Clipboard data for a note. + * + * Core's `WorkspaceCommentCopyData` carries only `commentState`; a note adds + * its own half so a pasted note keeps its title and colour. + */ +export interface NoteCopyData { + paster: string; + commentState: {[key: string]: unknown}; + noteState?: NoteState; +} diff --git a/blockly-workspace-notes/src/types/events.ts b/blockly-workspace-notes/src/types/events.ts new file mode 100644 index 0000000..4a5ca18 --- /dev/null +++ b/blockly-workspace-notes/src/types/events.ts @@ -0,0 +1,19 @@ +/** + * @fileoverview The JSON shape of the plugin's own undo event. + */ + +import type * as Blockly from 'blockly/core'; + +import type {NoteProperty, NotePropertyValue} from './note'; + +/** + * The JSON shape of a `NoteChange` event. + * + * Core's `CommentBaseJson` declares only `commentId`, so the three fields this + * event adds need declaring for `toJson` to be assignable to the base. + */ +export interface NoteChangeJson extends Blockly.Events.CommentBaseJson { + property?: NoteProperty; + oldValue?: NotePropertyValue; + newValue?: NotePropertyValue; +} diff --git a/blockly-workspace-notes/src/types/note.ts b/blockly-workspace-notes/src/types/note.ts new file mode 100644 index 0000000..5ce8bfd --- /dev/null +++ b/blockly-workspace-notes/src/types/note.ts @@ -0,0 +1,85 @@ +/** + * @fileoverview What a note carries beyond a workspace comment, and the + * surface the mixin adds to expose it. + * + * `NoteState` is the centre of this file and of the plugin: it is the record + * that is written to save files, restored by `migrate`, snapshotted onto undo + * events and copied to the clipboard. It outlives any single release, so it is + * named once here rather than re-derived wherever it is touched. + */ + +/** + * Who made a note and when. + * + * The timestamps are ISO 8601 strings rather than `Date` objects because they + * are written to JSON and read back verbatim. + */ +export interface NoteMeta { + author: string; + createdAt: string; + updatedAt: string; +} + +/** + * Everything a note carries beyond what a workspace comment already has. + */ +export interface NoteState { + title: string; + colour: string; + pinned: boolean; + locked: boolean; + zIndex: number; + meta: NoteMeta; +} + +/** + * Which properties `applyNoteProperty` understands. + * + * `'*'` means "replace the whole state", which is how a paste and an undo + * restore a note in one step. + */ +export type NoteProperty = + 'title' | 'colour' | 'pinned' | 'locked' | 'zIndex' | 'meta' | '*'; + +/** + * The value that goes with each `NoteProperty`. + * + * This is what the `{*}` annotation stood in for: the switch in + * `applyNoteProperty` writes into a string slot, a boolean slot, a number slot, + * an object slot and a whole-state slot, and only the property name says which. + */ +export type NotePropertyValue = + string | boolean | number | Partial | NoteState | null; + +/** + * Everything the note mixin adds to a workspace comment. + * + * Declared separately from the mixin because TypeScript cannot name an + * anonymous class expression in a `.d.ts`. Without this interface, + * `declaration: true` fails on `NoteComment` and `Note` with "has or is using + * private name". + */ +export interface NoteSurface { + getNoteState(): NoteState; + getTitle(): string; + setTitle(title: string): void; + getColour(): string; + setColour(colour: string): void; + isPinned(): boolean; + setPinned(pinned: boolean): void; + isLocked(): boolean; + setLocked(locked: boolean): void; + getZIndex(): number; + setZIndex(zIndex: number): void; + getMeta(): NoteMeta; + restoreMeta(meta: Partial): void; + touchMeta(): void; + saveNoteState(): NoteState; + applyNoteProperty(property: NoteProperty, value?: NotePropertyValue): void; + renderTitle(): void; + renderColour(): void; + applyPinned(): void; + applyLocked(): void; + applyZIndex(): void; + bringToFront(): void; +} diff --git a/blockly-workspace-notes/src/types/options.ts b/blockly-workspace-notes/src/types/options.ts new file mode 100644 index 0000000..ca88302 --- /dev/null +++ b/blockly-workspace-notes/src/types/options.ts @@ -0,0 +1,32 @@ +/** + * @fileoverview What a host application passes in: the plugin's options bag, + * and the palette entries it may supply. + */ + +import type {LockPredicate} from '../ui/lock_permission'; + +/** + * One entry in a note palette. + * + * `hue` is what the picker sorts and derives from; `fill` is the resolved hex + * so the swatch does not have to recompute it. + */ +export interface PaletteEntry { + name: string; + hue: number; + fill: string; +} + +/** + * Everything `WorkspaceNotes` accepts. + */ +export interface WorkspaceNotesOptions { + palette?: PaletteEntry[]; + defaultSize?: {width: number; height: number}; + getAuthor?: () => string; + canToggleLock?: LockPredicate; + contextMenu?: boolean; + skipSerializerRegistration?: boolean; + emitLegacyComments?: boolean; + xmlSupport?: boolean; +} diff --git a/blockly-workspace-notes/src/types/serialization.ts b/blockly-workspace-notes/src/types/serialization.ts new file mode 100644 index 0000000..4439aa7 --- /dev/null +++ b/blockly-workspace-notes/src/types/serialization.ts @@ -0,0 +1,57 @@ +/** + * @fileoverview The on-disk shapes: one note, and the plugin's slice of a save + * file. + * + * These are a persisted format, so a change here is a change to files already + * written by earlier versions. Adding an optional field whose default is its + * own absence is not one: older files simply lack it and read as the default. + * Changing what an existing field means, its type or where it lives — or + * making one required — needs a `SCHEMA_VERSION` bump and a matching entry in + * `serialization/migrations.ts`. + */ + +import type {NoteMeta} from './note'; + +/** + * A note's serialized form, as it appears in a save file. + * + * Deliberately sparse: `saveNote` writes width and height always and everything + * else only when it differs from the default, so most notes serialize to a + * handful of keys. + */ +export interface SavedNote { + // Geometry and the fields core's own comment format carries. Every one is + // optional, matching `Blockly.serialization.workspaceComments.State`: a + // sparse file may omit any of them, and `appendNote` falls back to the + // note's own defaults rather than assuming a value is present. + id?: string; + x?: number; + y?: number; + width?: number; + height?: number; + text?: string; + collapsed?: boolean; + editable?: boolean; + movable?: boolean; + deletable?: boolean; + + // What a note adds. + title?: string; + colour?: string; + pinned?: boolean; + locked?: boolean; + zIndex?: number; + meta?: Partial; + + // Unknown keys are preserved rather than rejected, so a file written by a + // newer version of the plugin survives a load and re-save by an older one. + [key: string]: unknown; +} + +/** + * The plugin's own slice of a save file, under the `workspaceNotes` key. + */ +export interface NotesPayload { + version: number; + notes: SavedNote[]; +} diff --git a/blockly-workspace-notes/src/ui/colour_swatches.ts b/blockly-workspace-notes/src/ui/colour_swatches.ts new file mode 100644 index 0000000..c0d0192 --- /dev/null +++ b/blockly-workspace-notes/src/ui/colour_swatches.ts @@ -0,0 +1,88 @@ +/** + * @fileoverview The row of colour swatches the "Colour" menu item shows, and + * the styles for it. + * + * Blockly's context menu has no notion of submenus, but `displayText` accepts + * an HTMLElement — so the whole palette fits in one row rather than spilling + * seven entries into the menu. + * + * The stylesheet is registered here rather than in `ui/css.ts` because these + * rules style HTML inside the menu's widget div, not the SVG chrome of a note; + * keeping them beside the element they style is what stops one drifting from + * the other. + */ + +import * as Blockly from 'blockly/core'; + +import type {Note} from '../model/note'; +import type {PaletteEntry} from '../types/options'; +import {asOneUndoStep} from '../utils/undo'; + +/** + * Builds the row of colour swatches used as a menu item's display text. + * + * Blockly's context menu has no notion of submenus, but `displayText` accepts + * an HTMLElement — so the whole palette fits in one row rather than spilling + * seven entries into the menu. + * + * @param note The note to recolour. + * @param palette The + * swatches. + * @returns The swatch row. + */ +export function createSwatchRow( + note: Note, + palette: PaletteEntry[], +): HTMLElement { + const row = document.createElement('div'); + row.className = 'blocklyNoteSwatchRow'; + + for (const {name, fill} of palette) { + const swatch = document.createElement('button'); + swatch.type = 'button'; + swatch.className = 'blocklyNoteSwatch'; + // The swatch shows the note's colour itself, the way a block's colour + // reads in the toolbox. + swatch.style.backgroundColor = fill; + swatch.title = name; + swatch.setAttribute('aria-label', name); + if (note.getColour().toLowerCase() === fill.toLowerCase()) { + swatch.classList.add('blocklyNoteSwatchSelected'); + } + + swatch.addEventListener('pointerdown', (e) => { + // Stop the menu's own handler from also firing for this row. + e.stopPropagation(); + e.preventDefault(); + asOneUndoStep(() => note.setColour(fill)); + Blockly.ContextMenu.hide(); + }); + + row.appendChild(swatch); + } + + return row; +} + +Blockly.Css.register(` +.blocklyNoteSwatchRow { + display: flex; + gap: 6px; + padding: 2px 0; +} + +.blocklyNoteSwatch { + width: 18px; + height: 18px; + padding: 0; + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 4px; + cursor: pointer; +} + +/* #fc3 is the selection colour core uses for comments and blocks. */ +.blocklyNoteSwatchSelected { + outline: 2px solid #fc3; + outline-offset: 1px; +} +`); diff --git a/blockly-workspace-notes/src/ui/context_menu.ts b/blockly-workspace-notes/src/ui/context_menu.ts new file mode 100644 index 0000000..8eb02bd --- /dev/null +++ b/blockly-workspace-notes/src/ui/context_menu.ts @@ -0,0 +1,296 @@ +/** + * @fileoverview Context menu items for notes. + * + * Two things about core are worth knowing here. First, its comment menu items + * are *not* part of `registerDefaultOptions()` — `registerCommentOptions()` + * has to be called explicitly or there is no "Add Comment" at all. Second, its + * `commentCreate` hardcodes `new RenderedWorkspaceComment(...)` rather than + * going through `workspace.newComment()`, so overriding that method is not + * enough to make the menu produce notes; the item itself must be replaced. + * + * All three of core's items are replaced, the other two only so that the menu + * calls a note a note throughout rather than switching to "comment" for + * duplicate and delete. + */ + +import * as Blockly from 'blockly/core'; + +import {DEFAULT_PALETTE} from '../constants/colours'; +import {Note} from '../model/note'; +import {nextZIndex, previousZIndex} from '../model/stacking'; +import {msg} from '../utils/messages'; +import {canToggleLock} from './lock_permission'; +import {asOneUndoStep} from '../utils/undo'; +import {createSwatchRow} from './colour_swatches'; + +const ScopeType = Blockly.ContextMenuRegistry.ScopeType; + +/** + * How many plugin instances are holding these items. + * + * The context menu registry is a global singleton while the plugin is + * per-workspace, so registration is reference-counted the same way the + * serializer, paster and XML wrappers are. Without it a second workspace - or + * a host that re-injects one - throws on the first duplicate item id, and the + * first one out would take the menu away from the ones still running. + * + * The palette belongs to whichever instance registers first, matching what + * `registerNoteSerializers` does with its own options. + */ +let registrationCount = 0; + +/** IDs of the items this module registers, in the order they are added. */ +const NOTE_ITEM_IDS = [ + 'noteColour', + 'notePin', + 'noteLock', + 'noteCollapse', + 'noteBringToFront', + 'noteSendToBack', +]; + +/** + * @param scope The menu scope. + * @returns The note the menu was opened on, if any. + */ +function noteFromScope(scope: Blockly.ContextMenuRegistry.Scope): Note | null { + const comment = scope.comment; + return comment instanceof Note ? comment : null; +} + +/** + * Registers every note-related context menu item. + * + * @param options Registration options. + * @param options.palette The colour swatches to offer. + */ +export function registerNoteContextMenu({palette = DEFAULT_PALETTE} = {}) { + if (registrationCount++) return; + const registry = Blockly.ContextMenuRegistry.registry; + + // Core does not register these by default; without them there is no + // "Add Comment" entry to replace. + if (!registry.getItem('commentCreate')) { + Blockly.ContextMenuItems.registerCommentOptions(); + } + + // Core's three comment items call a note a comment. Each is replaced below, + // keeping its ID, weight and keyboard shortcut so any host code referring to + // it still finds it and the menu still shows the shortcut hint. The wording + // falls back to core's own for a plain comment, which a host can still + // create directly even with this plugin installed. + registry.unregister('commentDuplicate'); + registry.register({ + id: 'commentDuplicate', + scopeType: ScopeType.COMMENT, + weight: 1, + associatedKeyboardShortcut: 'duplicate', + displayText: (scope) => + noteFromScope(scope) + ? msg('DUPLICATE_NOTE', 'Duplicate note') + : msg('DUPLICATE_COMMENT', 'Duplicate Comment'), + // isCopyable, not isMovable: core defines the first as movable *and* + // deletable, so a locked note is already not copyable and this is what + // takes the item away. It also closes a smaller gap - on a note that was + // undeletable for any other reason the item used to show and then quietly + // do nothing, because the paste had no data to work from. + preconditionFn: (scope) => + scope.comment?.isCopyable() ? 'enabled' : 'hidden', + callback: (scope) => { + const comment = scope.comment; + const data = comment?.toCopyData(); + if (!comment || !data) return; + Blockly.clipboard.paste(data, comment.workspace); + }, + }); + + registry.unregister('commentDelete'); + registry.register({ + id: 'commentDelete', + scopeType: ScopeType.COMMENT, + weight: 6, + associatedKeyboardShortcut: 'delete', + displayText: (scope) => + noteFromScope(scope) + ? msg('DELETE_NOTE', 'Delete note') + : msg('REMOVE_COMMENT', 'Remove Comment'), + preconditionFn: (scope) => + scope.comment?.isDeletable() ? 'enabled' : 'hidden', + callback: (scope) => { + const comment = scope.comment; + if (!comment) return; + asOneUndoStep(() => comment.dispose()); + comment.workspace.getAudioManager().play('delete'); + }, + }); + + registry.unregister('commentCreate'); + registry.register({ + id: 'commentCreate', + scopeType: ScopeType.WORKSPACE, + weight: 8, + displayText: () => msg('ADD_COMMENT', 'Add Note'), + preconditionFn: (scope) => + scope.workspace?.isMutator ? 'hidden' : 'enabled', + callback: (scope, menuOpenEvent, menuSelectEvent, location) => { + const workspace = scope.workspace; + if (!workspace) return; + asOneUndoStep(() => { + const note = new Note(workspace); + note.moveTo( + Blockly.utils.svgMath.screenToWsCoordinates( + workspace, + new Blockly.utils.Coordinate(location.x, location.y), + ), + ); + note.setZIndex(nextZIndex(workspace)); + Blockly.getFocusManager().focusNode(note); + }); + }, + }); + + registry.register({ + id: 'noteColour', + scopeType: ScopeType.COMMENT, + weight: 3, + displayText: (scope) => { + const note = noteFromScope(scope); + return note ? createSwatchRow(note, palette) : ''; + }, + preconditionFn: (scope) => { + const note = noteFromScope(scope); + return note?.isEditable() ? 'enabled' : 'hidden'; + }, + // Selecting the row itself does nothing; the swatches handle their own + // clicks so that one press picks a colour and dismisses the menu. + callback: () => {}, + }); + + registry.register({ + id: 'notePin', + scopeType: ScopeType.COMMENT, + weight: 4, + displayText: (scope) => + noteFromScope(scope)?.isPinned() + ? msg('UNPIN_NOTE', 'Unpin note') + : msg('PIN_NOTE', 'Pin note'), + // A locked note nobody here may unlock must not be unpinnable either, or + // a note held in place deliberately can still be released and dragged off. + // This gates the item, not the flag: locking never writes `movable`. + preconditionFn: (scope) => { + const note = noteFromScope(scope); + if (!note) return 'hidden'; + return note.isLocked() && !canToggleLock(note) ? 'disabled' : 'enabled'; + }, + callback: (scope) => { + const note = noteFromScope(scope); + if (!note) return; + asOneUndoStep(() => { + const pinning = !note.isPinned(); + if (pinning) note.setZIndex(nextZIndex(note.workspace)); + note.setPinned(pinning); + }); + }, + }); + + registry.register({ + id: 'noteLock', + scopeType: ScopeType.COMMENT, + weight: 5, + displayText: (scope) => + noteFromScope(scope)?.isLocked() + ? msg('UNLOCK_NOTE', 'Unlock note') + : msg('LOCK_NOTE', 'Lock note'), + // The two refusals are not symmetric, and collapsing them loses the half + // that matters. A greyed "Unlock note" answers the question someone + // actually has in front of a note they cannot edit - it is locked, + // unlocking exists, it is not theirs to do - where hiding the item + // explains nothing and reads as a bug. A greyed "Lock note" on every + // unlocked note explains nothing either, and says it constantly. + preconditionFn: (scope) => { + const note = noteFromScope(scope); + if (!note) return 'hidden'; + if (canToggleLock(note)) return 'enabled'; + return note.isLocked() ? 'disabled' : 'hidden'; + }, + callback: (scope) => { + const note = noteFromScope(scope); + if (!note) return; + asOneUndoStep(() => note.setLocked(!note.isLocked())); + }, + }); + + // Core registers no collapse item for comments - registerCollapseExpandBlock + // is ScopeType.BLOCK only - so with the foldout arrow gone from the bar this + // is the only way to collapse a note. + registry.register({ + id: 'noteCollapse', + scopeType: ScopeType.COMMENT, + weight: 4.1, + displayText: (scope) => + noteFromScope(scope)?.isCollapsed() + ? msg('EXPAND_NOTE', 'Expand note') + : msg('COLLAPSE_NOTE', 'Collapse note'), + preconditionFn: (scope) => (noteFromScope(scope) ? 'enabled' : 'hidden'), + callback: (scope) => { + const note = noteFromScope(scope); + if (!note) return; + asOneUndoStep(() => note.setCollapsed(!note.isCollapsed())); + }, + }); + + registry.register({ + id: 'noteBringToFront', + scopeType: ScopeType.COMMENT, + weight: 4.2, + displayText: () => msg('NOTE_BRING_TO_FRONT', 'Bring to front'), + preconditionFn: (scope) => (noteFromScope(scope) ? 'enabled' : 'hidden'), + callback: (scope) => { + const note = noteFromScope(scope); + if (!note) return; + asOneUndoStep(() => note.setZIndex(nextZIndex(note.workspace))); + }, + }); + + registry.register({ + id: 'noteSendToBack', + scopeType: ScopeType.COMMENT, + weight: 4.3, + displayText: () => msg('NOTE_SEND_TO_BACK', 'Send to back'), + preconditionFn: (scope) => (noteFromScope(scope) ? 'enabled' : 'hidden'), + callback: (scope) => { + const note = noteFromScope(scope); + if (!note) return; + asOneUndoStep(() => note.setZIndex(previousZIndex(note.workspace))); + }, + }); +} + +/** + * Removes the note items and restores core's `commentCreate`. + */ +export function unregisterNoteContextMenu(): void { + if (--registrationCount > 0) return; + registrationCount = 0; + const registry = Blockly.ContextMenuRegistry.registry; + + for (const id of NOTE_ITEM_IDS) { + if (registry.getItem(id)) registry.unregister(id); + } + + // Hand core's own three items back, in the order it registers them. + if (registry.getItem('commentDuplicate')) { + registry.unregister('commentDuplicate'); + Blockly.ContextMenuItems.registerCommentDuplicate(); + } + + if (registry.getItem('commentDelete')) { + registry.unregister('commentDelete'); + Blockly.ContextMenuItems.registerCommentDelete(); + } + + if (registry.getItem('commentCreate')) { + registry.unregister('commentCreate'); + Blockly.ContextMenuItems.registerCommentCreate(); + } +} diff --git a/blockly-workspace-notes/src/ui/css.ts b/blockly-workspace-notes/src/ui/css.ts new file mode 100644 index 0000000..331c066 --- /dev/null +++ b/blockly-workspace-notes/src/ui/css.ts @@ -0,0 +1,523 @@ +/** + * @fileoverview Styles for workspace notes. + * + * Registered at module load: Blockly.Css.register only takes effect for + * injections that happen afterwards, so importing this plugin before calling + * Blockly.inject is required. + * + * A note is a Blockly comment, so it is shaped like one: square, thin + * bordered, with a title bar carrying its two controls. Almost everything here + * is a small adjustment to elements core already builds and already sizes - + * the card is core's own highlight rect, the bar and its buttons are core's + * own, and the writing area is core's own textarea - so that resizing, + * collapsing, selection and keyboard access keep working without help. + * + * Colours come from the --commentFillColour / --commentBorderColour custom + * properties core's comment stylesheet already reads, plus --noteInkColour of + * ours for everything written on the note. + * + * NOTE: this stylesheet is a JavaScript template literal. A backtick anywhere + * inside it, including in a comment quoting a selector, silently ends the + * literal. npm run build will not catch that, because it is a runtime error + * rather than a syntax one; npm test will. + */ + +import * as Blockly from 'blockly/core'; + +import { + NOTE_CLASS, + FOOTER_CLASS, + FOOTER_TEXT_CLASS, + LOCK_CLASS, + LOCKED_CLASS, + PIN_CLASS, + SELECTION_CLASS, + PINNED_CLASS, + TITLED_CLASS, + TITLE_CLASS, +} from '../constants/dom'; +import { + BAR_DELETE_NUDGE, + BAR_ICON_MARGIN, + BAR_ICON_SIZE, + BODY_INSET, + CARD_BORDER_WIDTH, + FOOTER_OPACITY, + GLYPH_STROKE_WIDTH, + OUTLINE_OFFSET, + OUTLINE_RADIUS, + OUTLINE_WIDTH, + PINNED_CARD_BORDER_WIDTH, + PLACEHOLDER_OPACITY, + SELECTION_STROKE_WIDTH, + FOOTER_FONT_SIZE, + FOOTER_HEIGHT, + SCROLLBAR_WIDTH, + TITLE_FONT_SIZE, + TOPBAR_HEIGHT, +} from '../constants/layout'; + +Blockly.Css.register(` +/* + * The card. This is core's own highlight rect, which it resizes on every + * pointer move of a drag; a note fills it, where a plain comment leaves it + * empty. Left square, as core draws it. + */ +.${NOTE_CLASS} .blocklyCommentHighlight { + fill: var(--commentFillColour); + stroke: var(--commentBorderColour); + stroke-width: ${CARD_BORDER_WIDTH}px; +} + +/* + * A pinned note is held in place, and says so twice: a marker at the head of + * the title row, and a heavier edge. + * + * Two quiet signals rather than one loud one. The edge is what carries at a + * glance across a busy workspace, and it is still legible when a long title has + * pushed the marker to the very corner of the note. + */ +.${NOTE_CLASS}.${PINNED_CLASS} .blocklyCommentHighlight { + stroke-width: ${PINNED_CARD_BORDER_WIDTH}px; +} + +/* + * The markers themselves: Tabler's pin and lock, stroked rather than filled so + * they sit at the weight of the heading they lead rather than as solid blots + * on the paper. + * + * display:none rather than visibility, matching how core hides its own bar + * buttons - CommentBarButton.canBeFocused() defers to checkVisibility(), so a + * display:none element is skipped by keyboard navigation rather than trapped + * on. It also keeps whichever marker is hidden out of the accessibility tree + * altogether, which matters here because the lock, unlike the pin, is + * labelled. + * + * pointer-events:none because the title row is the note's drag handle. A note + * has to stay draggable by the part of the row a marker occupies. + */ +.${NOTE_CLASS} .${PIN_CLASS}, +.${NOTE_CLASS} .${LOCK_CLASS} { + display: none; + fill: none; + stroke: var(--noteInkColour); + stroke-width: ${GLYPH_STROKE_WIDTH}px; + stroke-linecap: round; + stroke-linejoin: round; + pointer-events: none; +} + +/* + * One slot, and a lock takes it from a pin: a note that cannot be edited is + * the more urgent of the two facts, and the pin comes back the moment it is + * unlocked. + * + * The :not() carries its argument's specificity, so at four classes this is + * the single rule deciding when a pin paints - rather than a show rule and a + * second one undoing it, which is the arrangement that goes wrong later. + */ +.${NOTE_CLASS}.${PINNED_CLASS}:not(.${LOCKED_CLASS}) .${PIN_CLASS} { + display: block; +} + +/* + * A locked note gets no third border weight to go with this. Pinning already + * doubles the edge, and a note that is both would then be indistinguishable + * from one that is only pinned. The glyph and the missing bin say it instead. + */ +.${NOTE_CLASS}.${LOCKED_CLASS} .${LOCK_CLASS} { + display: block; +} + +/* + * The title bar, in the same shade as the border. + * + * Core already paints this rect from --commentBorderColour and a note simply + * lets it, so the two-tone note is Blockly's own model rather than anything + * built on top of it. Only the height is ours: core's 24px bar is sized for a + * strip of icons, and a note needs a line of title between two margins. Core + * measures this rect's rendered height and derives the writing area's offset + * from it, so the number has to be set here rather than drawn around. + */ +.${NOTE_CLASS} .blocklyCommentTopbarBackground { + height: ${TOPBAR_HEIGHT}px; +} + +/* + * Core's two bar buttons, collapse and delete. + * + * Core ships the delete button display:none, so showing it is the whole of + * that. Both keep everything core gives them - position, focus, ARIA, the + * collapse button's relabelling between "Collapse Comment" and "Expand + * Comment" - because they are still core's buttons; only their artwork is + * swapped, in renderColour, for a copy drawn in this note's ink. + * + * The size is core's own 20px. Its transform-origin is not: core hardcodes + * 12px 12px so the collapsed rule can rotate the chevron about its middle. + * That number is in user space, not relative to the icon's own box - it works + * for core because core's 24px bar leaves a margin of 2, putting a 20px icon's + * centre at 12. A 48px bar leaves a margin of 14, so the centre is 24, and + * using core's number would swing the chevron off the note entirely. + */ +.${NOTE_CLASS} .blocklyFoldoutIcon, +.${NOTE_CLASS} .blocklyDeleteIcon { + display: block; + width: ${BAR_ICON_SIZE}px; + height: ${BAR_ICON_SIZE}px; +} + +.${NOTE_CLASS} .blocklyFoldoutIcon { + transform-origin: ${BAR_ICON_MARGIN + BAR_ICON_SIZE / 2}px + ${BAR_ICON_MARGIN + BAR_ICON_SIZE / 2}px; +} + +/* + * Core insets this one by a doubled margin while the collapse button gets a + * single one, so without this the two ends of the bar do not match. See + * BAR_DELETE_NUDGE. + */ +.${NOTE_CLASS} .blocklyDeleteIcon { + transform: translateX(${BAR_DELETE_NUDGE}px); +} + +/* + * A locked note cannot be deleted, and this is what enforces it in the bar. + * + * Core's delete button does not check isDeletable() before acting: the menu + * item and the keyboard shortcut both do, but the button calls dispose on the + * comment view outright. So hiding it is not decoration. + * + * display, not visibility, and for a sharper reason than the markers above. + * CommentBarButton.isVisible() is checkVisibility(), which by default ignores + * the visibility property - so a button hidden that way would still report + * itself visible, stay in the keyboard tab order, and still delete the note. + * + * Three classes, because the rule showing both bar buttons is two. + */ +.${NOTE_CLASS}.${LOCKED_CLASS} .blocklyDeleteIcon { + display: none; +} + +/* + * The resize handle goes with it. Core already refuses to act on the handle - + * onResizePointerDown is gated on isEditable - so all this stops is a locked + * note offering a grab handle that does nothing when you pull it. + */ +.${NOTE_CLASS}.${LOCKED_CLASS} .blocklyResizeHandle { + display: none; +} + +/* + * Core styles no focus ring for these, so a keyboard user would otherwise get + * the browser's default outline on a bare . The note's own edge colour + * keeps it in the same family as everything else on the card. + */ +.${NOTE_CLASS} .blocklyFoldoutIcon:focus-visible, +.${NOTE_CLASS} .blocklyDeleteIcon:focus-visible { + outline: ${OUTLINE_WIDTH}px solid var(--noteInkColour); + outline-offset: ${OUTLINE_OFFSET}px; + border-radius: ${OUTLINE_RADIUS}px; +} + +/* + * You write on the card, not in a box on it. + * + * Core gives its textarea a fill and a 1px border, which is what makes a + * comment read as a panel - and a lighter bordered panel inset in a coloured + * body is precisely how Blockly draws a field on a block. Both come off here, + * so the body is one flat colour and the bar above is what divides it. Core's + * own 5px padding goes too, replaced by an even inset on all four sides so the + * text sits the same distance off the bar as off the edges. + */ +.${NOTE_CLASS} .blocklyMinimalBody { + box-sizing: border-box; + padding: ${BODY_INSET}px; + padding-bottom: ${BODY_INSET + FOOTER_HEIGHT}px; +} + +/* + * The writing area itself: core's fill, border and padding off, and the + * scrollbar quietened. + * + * Core's textarea scrolls with the platform's own bar, and on a system set to + * show scrollbars always - rather than as an overlay that fades - that is a + * full-width white track down the side of the card: a piece of furniture the + * note never asked for, on the one surface meant to hold nothing but words. + * + * So it is made quiet rather than removed. Removing it outright would take the + * only sign that there is more text below, on the one element of a note that + * can have more to show than fits. Instead the gutter is narrowed and always + * reserved - the text keeps its width whether the bar is painted or not, so + * nothing reflows - and the paint is what changes: nothing at rest, and while + * the pointer is on the note or the caret is in it, a thumb in the note's own + * edge colour, the same shade that draws the border and the title bar. + */ +.${NOTE_CLASS} .blocklyTextarea { + background-color: transparent; + color: var(--noteInkColour); + border: none; + padding: 0; + scrollbar-width: thin; + scrollbar-color: transparent transparent; +} + +.${NOTE_CLASS}:hover .blocklyTextarea, +.${NOTE_CLASS} .blocklyTextarea:focus { + scrollbar-color: var(--commentBorderColour) transparent; +} + +/* + * The same for engines without scrollbar-color (Safari, and Chrome before + * 121). Chrome 121+ ignores these once the standard properties above are set + * to anything but auto, so the two cannot both apply and disagree. + */ +.${NOTE_CLASS} .blocklyTextarea::-webkit-scrollbar { + width: ${SCROLLBAR_WIDTH}px; +} + +.${NOTE_CLASS} .blocklyTextarea::-webkit-scrollbar-track { + background: transparent; +} + +.${NOTE_CLASS} .blocklyTextarea::-webkit-scrollbar-thumb { + background: transparent; + border-radius: ${SCROLLBAR_WIDTH / 2}px; +} + +.${NOTE_CLASS}:hover .blocklyTextarea::-webkit-scrollbar-thumb, +.${NOTE_CLASS} .blocklyTextarea:focus::-webkit-scrollbar-thumb { + background: var(--commentBorderColour); +} + +/* + * Where there is no pointer there is no hover, and a rule that only paints on + * hover would leave a touch device with a permanently invisible scrollbar - + * including while a finger is actually dragging the text. So on those the + * thumb is simply always painted; it is thin and in the paper's own colour, + * which was the point. + */ +@media (hover: none) { + .${NOTE_CLASS} .blocklyTextarea { + scrollbar-color: var(--commentBorderColour) transparent; + } + + .${NOTE_CLASS} .blocklyTextarea::-webkit-scrollbar-thumb { + background: var(--commentBorderColour); + } +} + +/* Bring the resize handle inside the sheet instead of over its corner. */ +.${NOTE_CLASS} .blocklyResizeHandle { + transform: translate(-${BODY_INSET}px, -${BODY_INSET}px); +} + +.blocklyRTL .${NOTE_CLASS} .blocklyResizeHandle { + transform: scale(-1, 1) translate(-${BODY_INSET}px, -${BODY_INSET}px); +} + +/* + * Title. A heading on the card, so it takes the weight of one, and a click + * opens its editor the way a click on a field does. + * + * central rather than middle: middle sits the text half an x-height above + * the baseline, which leaves it about a pixel high of the row it shares with + * the two glyphs. Both keywords are independent of the letters used - a title + * full of descenders lands where an all-caps one does - so this is simply the + * one that centres. + * + * The type is set below rather than here: the renderer writes the font + * SHORTHAND, which resets weight and size, so a rule at this specificity + * would lose both. + */ +.${TITLE_CLASS} { + dominant-baseline: central; + user-select: none; + cursor: text; +} + +.blocklyReadonly.blocklyComment .${TITLE_CLASS} { + cursor: inherit; +} + +/* + * The selectors below are deliberately over-specific. The renderer generates + * a .blocklyText rule with fill #fff at three classes + * (.thrasos-renderer.classic-theme .blocklyText), so a single-class rule + * loses to it and the title comes out white on pale paper; four here means + * the outcome does not depend on which stylesheet was injected last. + * + * The type has to be set here for the same reason, and it is the sharper of + * the two traps: alongside that fill rule the renderer writes + * + * .thrasos-renderer.classic-theme .blocklyText { + * font: normal 11pt sans-serif; + * } + * + * and font is a SHORTHAND, so it resets font-weight and font-size to the + * theme's field values every time. A plain .blocklyNoteTitle { font-weight: + * bold } is therefore not merely outranked, it is overwritten - which is why + * the title rendered at body weight and body size, indistinguishable from the + * text it heads. + */ +.${NOTE_CLASS}.blocklyComment .${TITLE_CLASS}.blocklyText { + fill: var(--noteInkColour); + font-size: ${TITLE_FONT_SIZE}px; + font-weight: bold; +} + +/* + * An unnamed note shows a placeholder rather than an empty row, greyed so it + * reads as a prompt and not as a title someone typed. + */ +.${NOTE_CLASS}.blocklyComment:not(.${TITLED_CLASS}) + .${TITLE_CLASS}.blocklyText { + fill: var(--noteInkColour); + opacity: ${PLACEHOLDER_OPACITY}; +} + +.blocklyRTL .${TITLE_CLASS} { + /* Revert the top bar's mirroring, matching core's .blocklyCommentPreview. */ + transform: scale(-1, 1); + direction: rtl; +} + +/* + * Core's truncated body preview never shows: the title stands in for it, both + * on an expanded note and on a collapsed one. The second selector is needed + * because core reveals the preview once a comment collapses, with + * .blocklyCollapsed.blocklyComment .blocklyCommentPreview, which outranks a + * two-class rule. + */ +.${NOTE_CLASS} .blocklyCommentPreview, +.${NOTE_CLASS}.blocklyComment.blocklyCollapsed .blocklyCommentPreview { + visibility: hidden; +} + +/* + * The title's editor, which is meant to be invisible. + * + * .blocklyHtmlInput is Blockly's field editor, and a field editor is supposed + * to announce itself - it sits centred in a white box over the block. On a + * note that reads as a mode: a panel opens on the paper. Everything that draws + * the box comes off, so clicking the title just puts a caret in it and the + * text carries on looking like the heading it already was. + * + * The background is the browser's own input default rather than anything + * Blockly sets, which is why it has to be cleared explicitly. + * + * The type is not here: the size is scaled by the workspace zoom, and the + * weight has to beat a three-class renderer rule, so title_editor.ts sets + * both inline where the scale is known. + */ +.blocklyNoteTitleInput { + background: transparent; + text-align: left; + padding: 0; +} + +/* + * Only the fade is set here. The colour itself is applied inline by + * title_editor.ts, because the editor lives in Blockly's WidgetDiv - a + * sibling of the workspace, not a descendant of the note - so the note's own + * --noteInkColour never reaches it. + */ +.blocklyNoteTitleInput::placeholder { + opacity: ${PLACEHOLDER_OPACITY}; +} + +.blocklyRTL .blocklyNoteTitleInput { + text-align: right; +} + +/* The title is hidden while its editor is open, as a field's label is. */ +.blocklyEditing .${TITLE_CLASS} { + visibility: hidden; +} + +/* + * Selection, last in this sheet and over-specific on purpose. + * + * Core strokes its own highlight rect, which for a note is the card - painted + * before the title bar, so the bar covers the ring's inner half for its whole + * height and leaves it full thickness below. The ring gets its own rect, + * painted last, instead. Core's rules are switched off rather than overridden, + * both the expanded one and the collapsed pair that moves the ring onto the + * bar. + * + * #fc3 is Blockly's selection colour, not the note's, and stays fixed: a + * selected note should look selected the same way a selected block does. + */ +.blocklySelected.${NOTE_CLASS} .blocklyCommentHighlight, +.blocklySelected.${NOTE_CLASS}.blocklyCollapsed .blocklyCommentHighlight, +.blocklySelected.${NOTE_CLASS}.blocklyCollapsed + .blocklyCommentTopbarBackground { + stroke: none; +} + +/* + * The footer: the author and the date, in the note's ink at a caption's + * weight. Faded rather than given a second colour, so it recedes without + * leaving the palette. + * + * Hidden on a collapsed note, which has no body for it to sit under, and on + * one carrying neither an author nor a date - renderFooter sets the attribute, + * because a group whose children are all display:none still has a box and + * would leave a blank strip reserved at the foot. + */ +.${NOTE_CLASS} .${FOOTER_CLASS} { + fill: none; + stroke: var(--noteInkColour); + stroke-width: ${GLYPH_STROKE_WIDTH}px; + stroke-linecap: round; + stroke-linejoin: round; + opacity: ${FOOTER_OPACITY}; + pointer-events: none; +} + +/* + * Carries blocklyText so it inherits the workspace's font rather than falling + * through to the SVG default, which is a serif. That means the renderer's own + * .blocklyText rule applies too, and it sets font - a shorthand, so it + * resets size and weight - at three classes. Hence four here, the same + * arithmetic the title rule above is doing and for the same reason. + */ +.${NOTE_CLASS}.blocklyComment .${FOOTER_TEXT_CLASS}.blocklyText { + fill: var(--noteInkColour); + stroke: none; + font-size: ${FOOTER_FONT_SIZE}px; + font-weight: normal; + dominant-baseline: central; +} + +.${NOTE_CLASS}.blocklyCollapsed .${FOOTER_CLASS}, +.${NOTE_CLASS} .${FOOTER_CLASS}[data-empty='true'] { + display: none; +} + +/* + * The footer is in the root group, which core does not mirror, so unlike the + * title there is no mirror to undo here - setting the direction is the whole + * of it. + * + * That is also all it needs. text-anchor is left at its default of start, + * which anchors to the start of the inline base direction: the left edge under + * ltr and the right edge under rtl. Since renderFooter measures its offsets + * from the note's leading edge either way, the two agree without a second + * rule. Setting end here would anchor the left edge and run the text back + * across the note. + */ +.blocklyRTL .${NOTE_CLASS} .${FOOTER_TEXT_CLASS} { + direction: rtl; +} + +.${NOTE_CLASS} .${SELECTION_CLASS} { + fill: none; + stroke: none; + pointer-events: none; +} + +.blocklySelected.${NOTE_CLASS} .${SELECTION_CLASS} { + stroke: #fc3; + stroke-width: ${SELECTION_STROKE_WIDTH}px; +} +`); diff --git a/blockly-workspace-notes/src/ui/icons.ts b/blockly-workspace-notes/src/ui/icons.ts new file mode 100644 index 0000000..c7a6979 --- /dev/null +++ b/blockly-workspace-notes/src/ui/icons.ts @@ -0,0 +1,95 @@ +/** + * @fileoverview The glyphs a note draws, and how they are coloured. + * + * Two of the three are Blockly's own bar buttons, and core builds those as + * `` pointing at files in the host's media folder. An + * `` cannot be tinted — `fill` and `stroke` do not reach inside a + * referenced document — so a note could either keep core's buttons and accept + * their fixed navy, or draw its own and lose the keyboard navigation, focus + * handling and ARIA that come with `CommentBarButton`. + * + * It does neither. Core reads nothing off those elements but their bounding + * box, id and visibility, so the artwork can be replaced by rewriting `href` to + * a `data:` URI with the note's own ink colour already in it. The button stays + * core's; only the picture changes, and it changes again whenever the note is + * recoloured. + * + * The glyphs are Tabler's, all authored on a 24-unit grid at stroke-width 2 so + * the three read as one set. Scaled into a 16px box that lands at about 1.3px + * on screen - heavy enough to hold its own beside a bold heading. + */ + +import {GLYPH_STROKE_WIDTH} from '../constants/layout'; + +/** Tabler's `chevron-down`, for the collapse button. */ +export const CHEVRON_GLYPH = ['M6 9l6 6l6 -6']; + +/** Tabler's `trash`, for the delete button. */ +export const TRASH_GLYPH = [ + 'M4 7l16 0', + 'M10 11l0 6', + 'M14 11l0 6', + 'M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12', + 'M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3', +]; + +/** Tabler's `user`, for the author in the footer. */ +export const USER_GLYPH = [ + 'M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0', + 'M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2', +]; + +/** Tabler's `calendar-event`, for the date in the footer. */ +export const CALENDAR_GLYPH = [ + 'M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12', + 'M16 3l0 4', + 'M8 3l0 4', + 'M4 11l16 0', + 'M8 15h2v2h-2l0 -2', +]; + +/** + * Tabler's `pinned`, for the marker on a pinned note. + * + * Drawn inline rather than through a `data:` URI, because it is the plugin's + * own element rather than one of core's — see `model/note.ts`. + */ +export const PIN_GLYPH = [ + 'M9 4v6l-2 4v2h10v-2l-2 -4v-6', + 'M12 16l0 5', + 'M8 4l8 0', +]; + +/** + * Tabler's `lock`, for the marker on a locked note. + * + * Drawn inline for the same reason as the pin: it is the plugin's own element + * rather than one of core's, so CSS can stroke it in the note's ink directly. + */ +export const LOCK_GLYPH = [ + 'M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6', + 'M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0', + 'M8 11v-4a4 4 0 1 1 8 0v4', +]; + +/** + * Renders a glyph as a `data:` URI, stroked in the given colour. + * + * Percent-encoded rather than base64: it stays readable in the DOM inspector, + * which matters when the only way to check the colour followed the note is to + * look. `#` has to be encoded whatever else is left alone, since it would + * otherwise start the URI's fragment and truncate the colour. + * + * @param paths The glyph's path data, on a 24-unit grid. + * @param colour The stroke colour. + * @returns A `data:image/svg+xml` URI. + */ +export function glyphToDataUri(paths: string[], colour: string): string { + const svg = + `` + + paths.map((d) => ``).join('') + + ``; + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +} diff --git a/blockly-workspace-notes/src/ui/lock_permission.ts b/blockly-workspace-notes/src/ui/lock_permission.ts new file mode 100644 index 0000000..64946e9 --- /dev/null +++ b/blockly-workspace-notes/src/ui/lock_permission.ts @@ -0,0 +1,68 @@ +/** + * @fileoverview Who is allowed to lock and unlock a note. + * + * Locking is the one thing a note does that is a matter of permission rather + * than preference, and the plugin has no idea what a host's permissions are. + * So the host supplies a predicate and this module holds it. + * + * It is held per workspace, deliberately. Every other registration the plugin + * makes is reference-counted and first-registration-wins, because the Blockly + * registries it writes into are global singletons and sharing a palette + * between two workspaces is harmless. Sharing a *permission* is not: a host + * running an authoring workspace beside a read-only one would hand whichever + * plugin instance happened to initialize first the final say over both. The + * WeakMap here is what keeps the two apart, and it lets a disposed workspace + * be collected even if `dispose` is never called. + * + * This gates the context menu, and nothing else. `setLocked` still works from + * code — it has to, or undo, paste and loading a file would all break — so + * this is an affordance, not a security boundary. A host that needs the + * guarantee enforces it where it saves. + */ + +import type * as Blockly from 'blockly/core'; + +import type {Note} from '../model/note'; +import type {NoteComment} from '../model/note_comment'; + +/** Decides whether a note's lock may be changed by hand. */ +export type LockPredicate = (note: Note | NoteComment) => boolean; + +/** The predicate each workspace was given, if it was given one. */ +const predicates = new WeakMap(); + +/** + * Records who may lock and unlock notes on a workspace. + * + * @param workspace The workspace the rule applies to. + * @param predicate The host's rule. + */ +export function setLockPermission( + workspace: Blockly.Workspace, + predicate: LockPredicate, +): void { + predicates.set(workspace, predicate); +} + +/** + * Forgets a workspace's rule, restoring the permissive default. + * + * @param workspace The workspace to forget. + */ +export function clearLockPermission(workspace: Blockly.Workspace): void { + predicates.delete(workspace); +} + +/** + * Asks whether this note's lock may be changed by hand. + * + * Permissive when no rule was set, so a host that never mentions locking gets + * a lock that simply works. + * + * @param note The note in question. + * @returns Whether the menu should offer to lock or unlock it. + */ +export function canToggleLock(note: Note | NoteComment): boolean { + const predicate = predicates.get(note.workspace); + return predicate ? !!predicate(note) : true; +} diff --git a/blockly-workspace-notes/src/ui/title_editor.ts b/blockly-workspace-notes/src/ui/title_editor.ts new file mode 100644 index 0000000..3305141 --- /dev/null +++ b/blockly-workspace-notes/src/ui/title_editor.ts @@ -0,0 +1,173 @@ +/** + * @fileoverview Editing a note's title in place. + * + * Blockly never asks for text in a dialog. A field on a block is edited where + * it sits: `FieldInput` puts an `` inside + * `WidgetDiv`, positions it over the field, and commits on Enter or blur. + * This does the same over a note's title. + * + * The difference from a field is that none of it should be visible. A field + * editor is a white box that opens over the block, which on a note reads as a + * mode rather than as typing; the stylesheet strips the box, this places the + * editor exactly over the title, and the caret goes to the end rather than + * selecting the line. The effect is that the heading simply becomes typeable, + * the way the body below it always is. + * + * The alternative, `Blockly.dialog.prompt`, falls back to the browser's own + * `window.prompt` unless the host has replaced it — a serif system dialog + * that matches nothing else on the page. + */ + +import * as Blockly from 'blockly/core'; + +import {TITLE_CLASS, UNTITLED_TITLE_TEXT} from '../constants/dom'; +import { + BAR_INSET, + TITLE_FONT_SIZE, + TITLE_LINE_HEIGHT, +} from '../constants/layout'; +import type {Note} from '../model/note'; +import {inkFor} from '../utils/colour'; +import {asOneUndoStep} from '../utils/undo'; + +/** + * Where the editor should sit, in viewport pixels. + */ +interface EditorBox { + left: number; + top: number; + width: number; + height: number; +} + +/** + * Returns where the editor should sit, in screen pixels. + * + * Taken from the title's own client rect, so the editor lands exactly over + * the text it replaces whatever the workspace scale and scroll are. Every + * note has a title row — an unnamed one shows a placeholder — so there is + * always something to measure. + * + * @param note The note being renamed. + * @returns A DOMRect-like box, or null if nothing is rendered. + */ +function editorBox(note: Note): EditorBox | null { + const title = note.getSvgRoot().querySelector(`.${TITLE_CLASS}`); + const box = title?.getBoundingClientRect(); + if (!box?.width) return null; + + // Left and top come straight from the title, so the text does not shift by + // a pixel when the editor opens over it. + const scale = note.workspace.getAbsoluteScale(); + // Room to type past the end of the current title, but never past the paper: + // the editor is transparent, so anything overflowing would be text floating + // on the canvas. It stops at the delete button rather than the note's edge, + // for the same reason the title itself truncates there. + const room = + note.getSvgRoot().getBoundingClientRect().right - + box.left - + BAR_INSET * scale; + return { + left: box.left, + top: box.top, + width: Math.max( + Math.min(Math.max(box.width * 2, TITLE_LINE_HEIGHT * 6 * scale), room), + box.width, + ), + height: box.height, + }; +} + +/** + * Opens an inline editor over a note's title. + * + * Enter or a click elsewhere commits; Escape restores the original title. The + * whole edit is one undo step. + * + * @param note The note to rename. + */ +export function editTitle(note: Note): void { + if (!note.workspace?.rendered || note.isDeadOrDying()) return; + + const box = editorBox(note); + if (!box) return; + + let cancelled = false; + let input: HTMLInputElement | null = null; + + const commit = () => { + if (cancelled || !input) return; + const title = input.value; + asOneUndoStep(() => note.setTitle(title)); + }; + + Blockly.WidgetDiv.show( + note, + note.workspace.RTL, + () => { + commit(); + Blockly.utils.dom.removeClass(note.getSvgRoot(), 'blocklyEditing'); + }, + note.workspace, + ); + + const div = Blockly.WidgetDiv.getDiv(); + if (!div) return; + + // The heading's own size, not the field size core uses for its editors: the + // editor is meant to be invisible, and text that changes size the moment a + // caret lands in it is the most visible thing a field editor can do. Scaled + // like every other on-canvas measurement, since the widget div is in page + // pixels while the title is in workspace units. + const fontSize = `${TITLE_FONT_SIZE * note.workspace.getAbsoluteScale()}px`; + div.style.fontSize = fontSize; + div.style.width = `${box.width}px`; + div.style.height = `${box.height}px`; + + // WidgetDiv is positioned relative to its parent, not to the page. + const parent = div.parentElement?.getBoundingClientRect(); + div.style.left = `${box.left - (parent?.left ?? 0) - window.scrollX}px`; + div.style.top = `${box.top - (parent?.top ?? 0) - window.scrollY}px`; + + input = document.createElement('input'); + // Blockly's own class, so the editor inherits the field styling core + // already ships rather than inventing a second look. + input.className = 'blocklyHtmlInput blocklyNoteTitleInput'; + input.setAttribute('spellcheck', 'false'); + input.setAttribute('aria-label', 'Note title'); + // The same greyed placeholder the note itself shows, so opening the editor + // on an unnamed note changes nothing on screen but the caret. + input.setAttribute('placeholder', UNTITLED_TITLE_TEXT); + input.style.fontSize = fontSize; + // Inline, like the size above, and for a sharper reason: the renderer ships + // .thrasos-renderer.classic-theme .blocklyHtmlInput { font-weight: normal }, + // three classes that outrank any single-class rule the stylesheet can give + // .blocklyNoteTitleInput. Left to CSS the heading would drop to book weight + // the instant the caret landed in it. + input.style.fontWeight = 'bold'; + // Inline for the same reason as the type above, and one of its own: the + // editor is in WidgetDiv, outside the note's SVG, so it inherits none of the + // note's colour custom properties. + input.style.color = inkFor(note.getColour()); + input.value = note.getTitle(); + div.appendChild(input); + + Blockly.utils.dom.addClass(note.getSvgRoot(), 'blocklyEditing'); + + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.stopPropagation(); + Blockly.WidgetDiv.hide(); + } else if (e.key === 'Escape') { + e.stopPropagation(); + cancelled = true; + Blockly.WidgetDiv.hide(); + } + }); + + input.focus({preventScroll: true}); + // The caret goes to the end rather than selecting the line: a selected title + // is the visual cue of a field editor opening, which is the thing this is + // trying not to look like. Select-all is still one keystroke away. + input.setSelectionRange(input.value.length, input.value.length); +} diff --git a/blockly-workspace-notes/src/utils/colour.ts b/blockly-workspace-notes/src/utils/colour.ts new file mode 100644 index 0000000..c261961 --- /dev/null +++ b/blockly-workspace-notes/src/utils/colour.ts @@ -0,0 +1,105 @@ +/** + * @fileoverview Deriving a note's writing surface and edge from its colour. + * + * A note is paper. Its colour is a hue at a fixed saturation and value, as a + * block's is, but a pale one — where Blockly's `hueToHex` is S 0.45 at V 0.65 + * and carries a white label, a note is a light wash carrying black text. + * + * One stored colour paints the body. Two values are read off it: the edge, a + * step down in value, which draws the border and the title bar; and the ink, + * darker still, which draws everything written on the note. + */ + +import * as Blockly from 'blockly/core'; + +import { + EDGE_VALUE_SCALE, + INK_SATURATION_MAX, + INK_SATURATION_SCALE, + INK_VALUE, + NOTE_SATURATION, + NOTE_VALUE, +} from '../constants/colours'; + +/** + * Converts a hex colour to HSV. + * + * Blockly offers `hsvToHex` but no inverse, so this supplies it. Hue is in + * degrees, saturation is 0-1 and value is 0-255, matching what `hsvToHex` + * expects back. + * + * @param hex A hex colour such as '#5b80a5'. + * @returns `[hue, saturation, value]`. + */ +export function hexToHsv(hex: string): [number, number, number] { + const [red, green, blue] = Blockly.utils.colour + .hexToRgb(hex) + .map((channel) => channel / 255); + + const max = Math.max(red, green, blue); + const min = Math.min(red, green, blue); + const delta = max - min; + + let hue = 0; + if (delta) { + if (max === red) hue = 60 * (((green - blue) / delta) % 6); + else if (max === green) hue = 60 * ((blue - red) / delta + 2); + else hue = 60 * ((red - green) / delta + 4); + } + if (hue < 0) hue += 360; + + return [hue, max ? delta / max : 0, max * 255]; +} + +/** + * Derives the shade a note's edges are drawn in. + * + * The same hue a step down in value. It draws the note's border and its title + * bar — which is exactly what Blockly's own comment does, since core paints + * `.blocklyCommentTopbarBackground` from `--commentBorderColour` already. + * + * @param colour The note's colour, as a hex string. + * @returns The edge colour as hex. + */ +export function edgeFor(colour: string): string { + const [hue, saturation, value] = hexToHsv(colour); + return Blockly.utils.colour.hsvToHex( + hue, + saturation, + value * EDGE_VALUE_SCALE, + ); +} + +/** + * Derives the colour everything written on a note is drawn in. + * + * The same hue, pushed dark and saturated enough to read on both the note's + * body and its title bar. Used for the title, the body text and the glyphs in + * the bar, so a note is one colour throughout rather than a coloured card with + * black furniture on it. + * + * @param colour The note's colour, as a hex string. + * @returns The ink colour as hex. + */ +export function inkFor(colour: string): string { + const [hue, saturation] = hexToHsv(colour); + return Blockly.utils.colour.hsvToHex( + hue, + Math.min(saturation * INK_SATURATION_SCALE, INK_SATURATION_MAX), + INK_VALUE * 255, + ); +} + +/** + * Returns the note colour for a hue. + * + * The note equivalent of Blockly's `hueToHex`, at the palette's own + * saturation and value rather than Blockly's, so notes stay distinct from the + * blocks they annotate. + * + * @param hue A hue in degrees. + * @returns The colour as hex. + */ +export function colourForHue(hue: number): string { + return Blockly.utils.colour.hsvToHex(hue, NOTE_SATURATION, NOTE_VALUE * 255); +} diff --git a/blockly-workspace-notes/src/utils/guards.ts b/blockly-workspace-notes/src/utils/guards.ts new file mode 100644 index 0000000..a095d96 --- /dev/null +++ b/blockly-workspace-notes/src/utils/guards.ts @@ -0,0 +1,40 @@ +/** + * @fileoverview Recognising a note among a workspace's comments. + * + * A workspace hands back `WorkspaceComment`s, and a host can still create a + * plain one alongside its notes, so every place that walks the comment list + * narrows it through here first. + * + * The check reads the mark `model/note_mixin.ts` puts on every note, rather + * than testing `instanceof` against the two classes. Two reasons. It keeps + * this module at the bottom of the import graph - importing the classes here + * is what used to close the loop through `model/stacking.ts` - and a symbol + * read is not defeated by the plugin being bundled twice, which `instanceof` + * is. It is still not duck typing: a plain comment carrying a `title` property + * has no way to be carrying `NOTE_BRAND` as well. + * + * The classes are imported for their types alone, and that import must stay + * written as `import type`: relying on TypeScript to elide it would put the + * cycle back the day someone turns on `verbatimModuleSyntax`. + * + * Note that `ui/context_menu.ts` and `clipboard/note_paster.ts` still use + * `instanceof Note` directly, and so are narrower than this - they want the + * rendered class specifically. The two have never agreed anyway: a headless + * `NoteComment` passes `isNote` and fails `instanceof Note`. + */ + +import {NOTE_BRAND} from '../constants/brand'; +import type {Note} from '../model/note'; +import type {NoteComment} from '../model/note_comment'; + +/** + * @param candidate Any value. + * @returns Whether the value is a note. + */ +export function isNote(candidate: unknown): candidate is Note | NoteComment { + return ( + typeof candidate === 'object' && + candidate !== null && + (candidate as Record)[NOTE_BRAND] === true + ); +} diff --git a/blockly-workspace-notes/src/utils/messages.ts b/blockly-workspace-notes/src/utils/messages.ts new file mode 100644 index 0000000..326c2f7 --- /dev/null +++ b/blockly-workspace-notes/src/utils/messages.ts @@ -0,0 +1,20 @@ +/** + * @fileoverview Reading translatable strings out of Blockly's message table. + */ + +import * as Blockly from 'blockly/core'; + +/** + * Reads a Blockly message with a fallback, since `blockly/core` on its own + * ships no message table. + * + * Every string this plugin shows goes through here, so a host that wants to + * translate one only has to set the matching key on `Blockly.Msg`. + * + * @param key The message key. + * @param fallback The text to use when the key is unset. + * @returns The message. + */ +export function msg(key: string, fallback: string): string { + return Blockly.Msg[key] || fallback; +} diff --git a/blockly-workspace-notes/src/utils/undo.ts b/blockly-workspace-notes/src/utils/undo.ts new file mode 100644 index 0000000..45b1068 --- /dev/null +++ b/blockly-workspace-notes/src/utils/undo.ts @@ -0,0 +1,26 @@ +/** + * @fileoverview Grouping several changes into one undoable step. + */ + +import * as Blockly from 'blockly/core'; + +/** + * Runs a mutation as a single undoable step. + * + * A menu action that touches two properties — pinning a note and raising it, + * say — would otherwise take two presses of undo to reverse. Joining an + * existing group rather than starting a new one matters when the caller is + * already inside one, such as during a paste. + * + * @param mutate The mutation to perform. + * @returns Whatever the mutation returned. + */ +export function asOneUndoStep(mutate: () => T): T { + const existingGroup = Blockly.Events.getGroup(); + if (!existingGroup) Blockly.Events.setGroup(true); + try { + return mutate(); + } finally { + Blockly.Events.setGroup(existingGroup); + } +} diff --git a/blockly-workspace-notes/test/colour.mocha.js b/blockly-workspace-notes/test/colour.mocha.js new file mode 100644 index 0000000..59da2a7 --- /dev/null +++ b/blockly-workspace-notes/test/colour.mocha.js @@ -0,0 +1,163 @@ +/** + * @fileoverview Tests for the colours derived from a note's own colour. + * + * A note stores one colour and the chrome reads three: the paper, the edge + * that draws the border and the title bar, and the ink everything written on + * the note is drawn in. Each is a pure function of a hex string, so they are + * checked here rather than through a workspace. + */ + +import {assert} from 'chai'; + +import {DEFAULT_PALETTE} from '../src/index'; +import {colourForHue, edgeFor, hexToHsv, inkFor} from '../src/utils/colour'; + +/** + * @param {string} hex A hex colour. + * @returns {number} Its perceived lightness, as the HSV value scaled by how + * little colour is in it. Enough to order two shades of one hue. + */ +function lightness(hex) { + const [, saturation, value] = hexToHsv(hex); + return value * (1 - saturation / 2); +} + +/** + * Hues wrap, so two angles are compared the short way round. + * + * @param {number} a One hue in degrees. + * @param {number} b The other. + * @returns {number} The smaller angle between them. + */ +function hueGap(a, b) { + const gap = Math.abs(a - b) % 360; + return gap > 180 ? 360 - gap : gap; +} + +/** + * How far a derived hue may drift, in degrees. Not zero: a derivation lands on + * 8-bit channels, and the closer together they are the further that rounding + * moves the hue read back off them. + * @type {number} + */ +const HUE_TOLERANCE = 4; + +suite('Note colours', function () { + const colours = DEFAULT_PALETTE.map(({fill}) => fill); + + suite('edgeFor', function () { + test('the edge is darker than the paper', function () { + for (const colour of colours) { + assert.isBelow( + lightness(edgeFor(colour)), + lightness(colour), + `${colour} edged with ${edgeFor(colour)}`, + ); + } + }); + + test('it keeps the hue', function () { + for (const colour of colours) { + const [hue, saturation] = hexToHsv(colour); + if (saturation < 0.02) continue; + assert.isBelow( + hueGap(hue, hexToHsv(edgeFor(colour))[0]), + HUE_TOLERANCE, + colour, + ); + } + }); + }); + + test('the edge is a different shade from the paper', function () { + for (const colour of colours) { + assert.notEqual(edgeFor(colour), colour); + } + }); + + test('the derivation returns a hex colour, never NaN', function () { + // Sampled right around the hue circle, since hexToHsv branches on which + // channel is the maximum and the seven swatches do not cover every case. + for (let hue = 0; hue < 360; hue += 15) { + assert.match(edgeFor(colourForHue(hue)), /^#[0-9a-f]{6}$/, `${hue}deg`); + } + }); + + test('the palette stores the colours its own hues produce', function () { + for (const {name, hue, fill} of DEFAULT_PALETTE) { + // Grey is the exception: it is deliberately hueless, so no hue + // reproduces it. + if (name === 'Grey') continue; + assert.equal(colourForHue(hue), fill, name); + } + }); +}); + +/** + * @param {string} hex A hex colour. + * @returns {number} Its relative luminance, per WCAG. + */ +function luminance(hex) { + const channels = [1, 3, 5].map((i) => { + const c = parseInt(hex.slice(i, i + 2), 16) / 255; + return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + }); + return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]; +} + +/** + * @param {string} a A hex colour. + * @param {string} b Another. + * @returns {number} Their WCAG contrast ratio. + */ +function contrast(a, b) { + const light = Math.max(luminance(a), luminance(b)); + const dark = Math.min(luminance(a), luminance(b)); + return (light + 0.05) / (dark + 0.05); +} + +suite('Note ink', function () { + test('it keeps the hue it came from', function () { + for (const {fill} of DEFAULT_PALETTE) { + // Grey has no hue to keep, so there is nothing to compare. + if (hexToHsv(fill)[1] === 0) continue; + assert.closeTo( + hexToHsv(inkFor(fill))[0], + hexToHsv(fill)[0], + 1, + `ink for ${fill} drifted off its hue`, + ); + } + }); + + test('it is far darker than the note it is written on', function () { + for (const {fill} of DEFAULT_PALETTE) { + assert.isBelow( + hexToHsv(inkFor(fill))[2], + hexToHsv(fill)[2] / 2, + `ink for ${fill} is not dark enough to read as ink`, + ); + } + }); + + // The guard that matters. The title sits on the bar and the body text on the + // paper, so the ink has to clear 4.5:1 against both - on every palette + // colour, and on any a host substitutes at the same saturation. Changing a + // palette entry or an ink constant without checking this is how a note ends + // up unreadable. + test('it is readable on both the paper and the bar', function () { + for (const {name, fill} of DEFAULT_PALETTE) { + const ink = inkFor(fill); + assert.isAtLeast( + contrast(ink, fill), + 4.5, + `${name}: ink on the note body is too faint`, + ); + assert.isAtLeast( + contrast(ink, edgeFor(fill)), + 4.5, + `${name}: ink on the title bar is too faint`, + ); + } + }); +}); diff --git a/blockly-workspace-notes/test/diagrams.mocha.js b/blockly-workspace-notes/test/diagrams.mocha.js new file mode 100644 index 0000000..1c3e761 --- /dev/null +++ b/blockly-workspace-notes/test/diagrams.mocha.js @@ -0,0 +1,326 @@ +/** + * @fileoverview Asserts the documentation diagrams still draw what the code + * draws. + * + * They are hand-maintained SVG, and they had drifted: one file gave the title + * bar a height of 35 and the other 36, the collapsed card was a pixel short, + * the two disagreed about where the date sits, and the resize handle was drawn + * eight pixels above the box it actually occupies. None of that is visible by + * eye, and none of it would ever fail a build. + * + * So this reads both files and checks every number a constant can account for. + * + * What it deliberately does **not** check: colours, text content, the callout + * lines and labels, where body lines sit, the canvas size, element order and + * the glyph path data. Those are the parts a person should be free to redraw, + * and a test that pinned them would make the diagrams worse by making them + * expensive to improve. + * + * Elements are addressed by `data-part`, not by position, for the same reason. + */ + +import * as Blockly from 'blockly/core'; +import {assert} from 'chai'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { + BAR_DELETE_NUDGE, + BAR_ICON_MARGIN, + BAR_ICON_SIZE, + BAR_INSET, + BODY_INSET, + CARD_BORDER_WIDTH, + DEFAULT_SIZE, + FOOTER_HEIGHT, + FOOTER_ICON_SIZE, + FOOTER_LABEL_GAP, + FOOTER_FONT_SIZE, + GLYPH_GRID, + GLYPH_STROKE_WIDTH, + LOCK_GLYPH_INK, + MARKER_ICON_GAP, + MARKER_ICON_SIZE, + PINNED_CARD_BORDER_WIDTH, + PIN_GLYPH_INK, + RESIZE_HANDLE_SIZE, + SELECTION_STROKE_WIDTH, + TITLE_FONT_SIZE, + TOPBAR_HEIGHT, +} from '../src/constants/layout'; + +const IMAGES = path.join(process.cwd(), 'docs', 'images'); + +/** Everything rounds to four decimals in the files. */ +const EPSILON = 0.01; + +const BAR_SCALE = MARKER_ICON_SIZE / GLYPH_GRID; +const FOOTER_SCALE = FOOTER_ICON_SIZE / GLYPH_GRID; + +/** + * @param file A file name inside docs/images. + * @returns Its parsed root element. + */ +function readSvg(file) { + const full = path.join(IMAGES, file); + assert.isTrue(fs.existsSync(full), `expected a diagram at ${full}`); + return Blockly.utils.xml.textToDom(fs.readFileSync(full, 'utf8')); +} + +/** + * Reads the translate and uniform scale off a transform attribute. + * + * @param element An element carrying a transform. + * @returns Its translation and scale. + */ +function transformOf(element) { + const transform = element.getAttribute('transform') ?? ''; + const translate = /translate\(\s*(-?[\d.]+)[ ,]+(-?[\d.]+)\s*\)/.exec( + transform, + ); + const scale = /scale\(\s*(-?[\d.]+)/.exec(transform); + assert.isNotNull(translate, `no translate in "${transform}"`); + return { + tx: Number(translate[1]), + ty: Number(translate[2]), + scale: scale ? Number(scale[1]) : 1, + }; +} + +/** + * @param note A note group. + * @param name The `data-part` to find. + * @returns That element, or null when the note omits it. + */ +function part(note, name) { + return note.querySelector(`[data-part="${name}"]`); +} + +/** + * @param element An element. + * @param name An attribute on it. + * @returns That attribute as a number. + */ +function num(element, name) { + return Number(element.getAttribute(name)); +} + +/** + * @param root A parsed diagram. + * @returns Every note group in it, keyed by `data-note`. + */ +function notesIn(root) { + const groups = [...root.querySelectorAll('[data-note]')]; + assert.isAbove(groups.length, 0, 'no note groups found — check data-note'); + return new Map(groups.map((g) => [g.getAttribute('data-note'), g])); +} + +suite('Documentation diagrams', function () { + suiteSetup(function () { + this.diagrams = new Map([ + ['note-anatomy.svg', notesIn(readSvg('note-anatomy.svg'))], + ['note-states.svg', notesIn(readSvg('note-states.svg'))], + ]); + this.every = [...this.diagrams].flatMap(([file, notes]) => + [...notes].map(([kind, note]) => ({file, kind, note})), + ); + // A walk that found nothing would pass every assertion below. + assert.isAtLeast(this.every.length, 8, 'too few notes to be the real set'); + }); + + test('every card is the default size, or the bar when collapsed', function () { + for (const {file, kind, note} of this.every) { + const card = part(note, 'card'); + const where = `${file} ${kind}`; + assert.equal(num(card, 'width'), DEFAULT_SIZE.width, where); + assert.equal( + num(card, 'height'), + kind === 'collapsed' ? TOPBAR_HEIGHT : DEFAULT_SIZE.height, + where, + ); + // Drawn at the origin so the stroke straddles the edge, which is what + // core's highlight rect does. + assert.equal(num(card, 'x') || 0, 0, where); + assert.equal(num(card, 'y') || 0, 0, where); + assert.equal( + num(card, 'stroke-width'), + kind === 'pinned' ? PINNED_CARD_BORDER_WIDTH : CARD_BORDER_WIDTH, + where, + ); + assert.equal(num(part(note, 'bar'), 'height'), TOPBAR_HEIGHT, where); + } + }); + + test('the bar buttons sit where core puts them', function () { + for (const {file, kind, note} of this.every) { + const where = `${file} ${kind}`; + const foldout = transformOf(part(note, 'foldout')); + assert.closeTo(foldout.tx, BAR_ICON_MARGIN, EPSILON, where); + assert.closeTo(foldout.ty, BAR_ICON_MARGIN, EPSILON, where); + assert.closeTo(foldout.scale, BAR_SCALE, EPSILON, where); + + const del = part(note, 'delete'); + if (!del) continue; + const expected = + DEFAULT_SIZE.width - + BAR_ICON_SIZE - + BAR_ICON_MARGIN * 2 + + BAR_DELETE_NUDGE; + assert.closeTo(transformOf(del).tx, expected, EPSILON, where); + } + }); + + test('a marker is placed by its ink, and the title follows it', function () { + for (const {file, kind, note} of this.every) { + const where = `${file} ${kind}`; + const marker = part(note, 'marker'); + const ink = marker + ? kind.includes('locked') + ? LOCK_GLYPH_INK + : PIN_GLYPH_INK + : null; + + if (marker) { + const {tx, ty, scale} = transformOf(marker); + assert.closeTo(tx, BAR_INSET - ink.x * BAR_SCALE, EPSILON, where); + assert.closeTo( + ty, + TOPBAR_HEIGHT / 2 - (ink.y + ink.height / 2) * BAR_SCALE, + EPSILON, + where, + ); + assert.closeTo(scale, BAR_SCALE, EPSILON, where); + } + + const title = part(note, 'title'); + const advance = ink ? ink.width * BAR_SCALE + MARKER_ICON_GAP : 0; + assert.closeTo(num(title, 'x'), BAR_INSET + advance, EPSILON, where); + assert.closeTo(num(title, 'y'), TOPBAR_HEIGHT / 2, EPSILON, where); + assert.equal(num(title, 'font-size'), TITLE_FONT_SIZE, where); + } + }); + + test('the footer sits on its own row', function () { + const textY = DEFAULT_SIZE.height - BODY_INSET - FOOTER_HEIGHT / 2; + const iconY = textY - FOOTER_ICON_SIZE / 2; + for (const {file, kind, note} of this.every) { + const where = `${file} ${kind}`; + const dateIcon = part(note, 'footer-date-icon'); + if (!dateIcon) continue; + + for (const name of ['footer-author-icon', 'footer-date-icon']) { + const icon = part(note, name); + if (!icon) continue; + const {ty, scale} = transformOf(icon); + assert.closeTo(ty, iconY, EPSILON, `${where} ${name}`); + assert.closeTo(scale, FOOTER_SCALE, EPSILON, `${where} ${name}`); + } + + const authorIcon = part(note, 'footer-author-icon'); + if (authorIcon) { + assert.closeTo(transformOf(authorIcon).tx, BODY_INSET, EPSILON, where); + assert.closeTo( + num(part(note, 'footer-author-text'), 'x'), + BODY_INSET + FOOTER_ICON_SIZE + FOOTER_LABEL_GAP, + EPSILON, + where, + ); + } + + // The date's own offset stands in for a measured text width and has no + // constant, so only the gap after its glyph is checked here. + const dateText = part(note, 'footer-date-text'); + assert.closeTo( + num(dateText, 'x') - transformOf(dateIcon).tx, + FOOTER_ICON_SIZE + FOOTER_LABEL_GAP, + EPSILON, + where, + ); + for (const name of ['footer-author-text', 'footer-date-text']) { + const text = part(note, name); + if (!text) continue; + assert.closeTo(num(text, 'y'), textY, EPSILON, `${where} ${name}`); + assert.equal( + num(text, 'font-size'), + FOOTER_FONT_SIZE, + `${where} ${name}`, + ); + } + } + }); + + test('the two files agree on where the date sits', function () { + const offsets = new Set( + this.every + .map( + ({note}) => + part(note, 'footer-author-icon') && part(note, 'footer-date-icon'), + ) + .filter(Boolean) + .map((icon) => transformOf(icon).tx), + ); + assert.equal( + offsets.size, + 1, + `the date glyph stands in for one measured width, but the diagrams ` + + `place it at ${[...offsets].join(' and ')}`, + ); + }); + + test('the resize handle stays inside the box core gives it', function () { + const low = DEFAULT_SIZE.width - BODY_INSET - RESIZE_HANDLE_SIZE; + const lowY = DEFAULT_SIZE.height - BODY_INSET - RESIZE_HANDLE_SIZE; + let seen = 0; + for (const {file, kind, note} of this.every) { + const handle = part(note, 'resize-handle'); + if (!handle) continue; + seen++; + const {tx, ty, scale} = transformOf(handle); + const where = `${file} ${kind}`; + // The artwork is free to change; the box it is drawn in is not. + assert.closeTo(tx, low, EPSILON, where); + assert.closeTo(ty, lowY, EPSILON, where); + assert.closeTo(scale, RESIZE_HANDLE_SIZE / GLYPH_GRID, EPSILON, where); + } + assert.isAtLeast(seen, 1, 'no diagram draws the resize handle'); + }); + + test('a locked note has neither a delete button nor a handle', function () { + const locked = this.every.filter(({kind}) => kind.includes('locked')); + assert.isAtLeast(locked.length, 2, 'both diagrams should show one'); + for (const {file, kind, note} of locked) { + assert.isNull(part(note, 'delete'), `${file} ${kind} draws a bin`); + assert.isNull( + part(note, 'resize-handle'), + `${file} ${kind} draws a resize handle`, + ); + } + }); + + test('the selection ring straddles the card edge', function () { + for (const {file, kind, note} of this.every) { + const ring = part(note, 'selection'); + if (!ring) continue; + const where = `${file} ${kind}`; + assert.equal(num(ring, 'x') || 0, 0, where); + assert.equal(num(ring, 'y') || 0, 0, where); + assert.equal(num(ring, 'width'), DEFAULT_SIZE.width, where); + assert.equal(num(ring, 'height'), DEFAULT_SIZE.height, where); + assert.equal(num(ring, 'stroke-width'), SELECTION_STROKE_WIDTH, where); + } + }); + + test('every glyph is stroked at the authored weight', function () { + for (const {file, kind, note} of this.every) { + const glyphs = [...note.querySelectorAll('g[data-part]')]; + assert.isAbove(glyphs.length, 0, `${file} ${kind} has no glyphs`); + for (const glyph of glyphs) { + assert.equal( + num(glyph, 'stroke-width'), + GLYPH_STROKE_WIDTH, + `${file} ${kind} ${glyph.getAttribute('data-part')}`, + ); + } + } + }); +}); diff --git a/blockly-workspace-notes/test/imports.mocha.js b/blockly-workspace-notes/test/imports.mocha.js new file mode 100644 index 0000000..3c013af --- /dev/null +++ b/blockly-workspace-notes/test/imports.mocha.js @@ -0,0 +1,118 @@ +/** + * @fileoverview Asserts that `src/` has no circular imports. + * + * There used to be one, between `model/note.ts`, `model/stacking.ts` and + * `utils/guards.ts`, and it was documented as harmless. It was — but only by + * accident, and the two builds resolved it by different means: the production + * bundle flattens those modules into one scope, where a cycle is raw temporal + * dead zone ordering, while the test bundle keeps module boundaries and uses + * live bindings. So a cycle that broke would have broken in `dist/` and passed + * here. This test is what stops one coming back unnoticed. + * + * Only *value* imports count. `import type` is erased before the bundler ever + * sees it, so a type-level loop is not a cycle in any build. + * + * Deliberately conservative: a mixed `import {type Foo, Bar}` is read as a + * value edge. A false positive fails loudly and is fixed by splitting the + * import; a false negative would be invisible, which is the failure this test + * exists to prevent. + * + * Known blind spots, none of which the source uses today: `export * from`, + * dynamic `import()`, and path aliases. + */ + +import {assert} from 'chai'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** The source tree, resolved from the package root. */ +const SRC = path.join(process.cwd(), 'src'); + +/** + * @param dir A directory to walk. + * @returns Every `.ts` file under it, recursively. + */ +function walk(dir) { + return fs.readdirSync(dir, {withFileTypes: true}).flatMap((entry) => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return walk(full); + return entry.isFile() && full.endsWith('.ts') ? [full] : []; + }); +} + +/** + * Removes comments, so that an import statement quoted in a fileoverview is + * not mistaken for a real one. Several of them do exactly that. + * + * @param source A TypeScript source file. + * @returns The same source with block and line comments blanked. + */ +function stripComments(source) { + return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, ''); +} + +/** + * @param file An absolute path to a source file. + * @returns The files it imports at runtime, as absolute paths. + */ +function valueImportsOf(file) { + const source = stripComments(fs.readFileSync(file, 'utf8')) + // Whole type-only statements, which the compiler erases. + .replace(/\b(?:import|export)\s+type\s+[\s\S]*?from\s*['"][^'"]+['"]/g, ''); + + const specifiers = [ + // import … from './x' / export … from './x' + ...source.matchAll(/(?:import|export)\b[^;]*?from\s*['"](\.[^'"]+)['"]/g), + // import './x' — side-effect only, as plugin.ts does for the stylesheet. + ...source.matchAll(/import\s*['"](\.[^'"]+)['"]/g), + ].map((match) => match[1]); + + return specifiers + .map((specifier) => { + const base = path.resolve(path.dirname(file), specifier); + for (const candidate of [`${base}.ts`, path.join(base, 'index.ts')]) { + if (fs.existsSync(candidate)) return candidate; + } + return null; + }) + .filter((resolved) => resolved !== null); +} + +suite('Import graph', function () { + suiteSetup(function () { + // Without this, a wrong working directory yields an empty walk, no + // cycles, and a green test that checked nothing. + assert.isTrue( + fs.existsSync(path.join(SRC, 'index.ts')), + `expected the source tree at ${SRC}`, + ); + this.files = walk(SRC); + assert.isAbove(this.files.length, 30, 'the walk found too few files'); + }); + + test('no module imports itself, however indirectly', function () { + const graph = new Map( + this.files.map((file) => [file, valueImportsOf(file)]), + ); + const label = (file) => path.relative(SRC, file); + + const state = new Map(); // file -> 'open' | 'done' + const cycles = []; + + const visit = (file, trail) => { + if (state.get(file) === 'done') return; + if (state.get(file) === 'open') { + const loop = trail.slice(trail.indexOf(file)); + cycles.push([...loop, file].map(label).join(' -> ')); + return; + } + state.set(file, 'open'); + for (const next of graph.get(file) ?? []) visit(next, [...trail, file]); + state.set(file, 'done'); + }; + + for (const file of this.files) visit(file, []); + + assert.deepEqual(cycles, [], `circular imports:\n ${cycles.join('\n ')}`); + }); +}); diff --git a/blockly-workspace-notes/test/index.html b/blockly-workspace-notes/test/index.html new file mode 100644 index 0000000..dc96c57 --- /dev/null +++ b/blockly-workspace-notes/test/index.html @@ -0,0 +1,18 @@ + + + + + Blockly Plugin Test + + + + +
+ + + diff --git a/blockly-workspace-notes/test/index.js b/blockly-workspace-notes/test/index.js new file mode 100644 index 0000000..0a094ad --- /dev/null +++ b/blockly-workspace-notes/test/index.js @@ -0,0 +1,133 @@ +/** + * @fileoverview Playground for the workspace notes plugin. + * + * Rendered behaviour (dragging, resizing, the title in the top bar, the + * colour swatches) cannot be covered by the headless mocha tests, so this + * page is where it gets exercised. The "Save notes" / "Load notes" actions + * make the round-trip visible without opening devtools. + */ + +import * as Blockly from 'blockly'; +import {toolboxCategories, createPlayground} from '@blockly/dev-tools'; + +import {WorkspaceNotes} from '../src/index'; + +/** Holds the most recent save, so Load can restore it. */ +let savedState = null; + +/** The most recent XML save, for the XML round-trip actions. */ +let savedXml = null; + +/** + * Creates a workspace with the notes plugin attached. + * + * @param {HTMLElement} blocklyDiv The blockly container div. + * @param {!Blockly.BlocklyOptions} options The Blockly options. + * @returns {!Blockly.WorkspaceSvg} The created workspace. + */ +function createWorkspace(blocklyDiv, options) { + const workspace = Blockly.inject(blocklyDiv, options); + + const notes = new WorkspaceNotes(workspace, { + getAuthor: () => 'playground', + }); + notes.init(); + + // Handy for poking at things from the browser console. + globalThis.notesPlugin = notes; + + return workspace; +} + +document.addEventListener('DOMContentLoaded', function () { + const defaultOptions = { + toolbox: toolboxCategories, + }; + + createPlayground( + document.getElementById('root'), + createWorkspace, + defaultOptions, + ).then((playground) => { + playground.addAction('Add note', (workspace) => { + globalThis.notesPlugin.createNote({ + title: 'Note', + text: 'Say something...', + x: 40, + y: 40, + }); + console.log( + 'Notes on workspace:', + workspace.getTopComments(false).length, + ); + }); + + playground.addAction('Save notes', (workspace) => { + savedState = Blockly.serialization.workspaces.save(workspace); + console.log(JSON.stringify(savedState, null, 2)); + console.log( + 'workspaceComments key present:', + Object.hasOwn(savedState, 'workspaceComments'), + '| notes saved:', + savedState.workspaceNotes?.notes.length ?? 0, + ); + }); + + playground.addAction('Load notes', (workspace) => { + if (!savedState) { + console.warn('Nothing saved yet — press "Save notes" first.'); + return; + } + Blockly.serialization.workspaces.load(savedState, workspace); + console.log( + 'Notes after reload:', + workspace.getTopComments(false).length, + ); + }); + + playground.addAction('Save notes as XML', (workspace) => { + savedXml = Blockly.Xml.domToPrettyText( + Blockly.Xml.workspaceToDom(workspace), + ); + console.log(savedXml); + }); + + playground.addAction('Load notes from XML', (workspace) => { + if (!savedXml) { + console.warn('Nothing saved yet — press "Save notes as XML" first.'); + return; + } + Blockly.Xml.clearWorkspaceAndLoadFromXml( + Blockly.utils.xml.textToDom(savedXml), + workspace, + ); + console.log( + 'Notes after XML reload:', + workspace.getTopComments(false).length, + ); + }); + + playground.addAction('Load a legacy file', (workspace) => { + // No `workspaceNotes` key at all: what plain Blockly would have written. + Blockly.serialization.workspaces.load( + { + workspaceComments: [ + { + id: 'legacy1', + x: 60, + y: 220, + width: 220, + height: 110, + text: 'Saved before this plugin existed.', + }, + ], + }, + workspace, + ); + console.log( + 'Loaded legacy comment as a note:', + workspace.getCommentById('legacy1')?.constructor.name, + ); + }); + }); +}); diff --git a/blockly-workspace-notes/test/layout.mocha.js b/blockly-workspace-notes/test/layout.mocha.js new file mode 100644 index 0000000..d9d8fce --- /dev/null +++ b/blockly-workspace-notes/test/layout.mocha.js @@ -0,0 +1,74 @@ +/** + * @fileoverview Pins the resolved values in `constants/layout.ts`. + * + * Most of that file is arithmetic rather than literals, which is what keeps + * the note's proportions honest — but it also means a change to one number + * moves several others, and a well-meant tidy-up of a derivation can move a + * value without looking like it moved anything. These assertions are the + * arithmetic's answer sheet. + * + * Two of them exist for a specific trap. `FOOTER_ICON_SIZE` is derived from + * `TITLE_LINE_HEIGHT` and `FOOTER_FONT_SIZE` deliberately is not, because the + * same three-quarter step that gives an exact 12 gives 11.00025 against the + * body's 14.667px — and that would reach the stylesheet verbatim. The integer + * checks below are what catch a future attempt to make the two symmetrical. + */ + +import {assert} from 'chai'; + +import { + BAR_ICON_MARGIN, + BAR_ICON_SIZE, + BAR_INSET, + BODY_INSET, + DEFAULT_SIZE, + FOOTER_FONT_SIZE, + FOOTER_HANDLE_CLEARANCE, + FOOTER_HEIGHT, + FOOTER_ICON_SIZE, + GLYPH_GRID, + MARKER_ICON_SIZE, + MIN_SIZE, + RESIZE_HANDLE_SIZE, + SCROLLBAR_WIDTH, + TITLE_FONT_SIZE, + TITLE_LINE_HEIGHT, + TOPBAR_HEIGHT, +} from '../src/constants/layout'; + +suite('Layout constants', function () { + test('the note is built on a 16px line box', function () { + assert.equal(TITLE_LINE_HEIGHT, 16); + assert.equal(TITLE_FONT_SIZE, 16); + assert.equal(BAR_ICON_SIZE, 16); + assert.equal(MARKER_ICON_SIZE, 16); + assert.equal(FOOTER_HEIGHT, 16); + }); + + test('the footer is three quarters of the bar', function () { + assert.equal(FOOTER_ICON_SIZE, 12); + assert.equal(FOOTER_FONT_SIZE, 11); + // Both must stay whole: a fraction here reaches the stylesheet as one. + assert.isTrue(Number.isInteger(FOOTER_ICON_SIZE)); + assert.isTrue(Number.isInteger(FOOTER_FONT_SIZE)); + }); + + test('the title bar geometry resolves as drawn', function () { + assert.equal(TOPBAR_HEIGHT, 36); + assert.equal(BAR_ICON_MARGIN, 10); + assert.equal(BAR_INSET, 31); + assert.equal(GLYPH_GRID, 24); + }); + + test('the body and footer resolve as drawn', function () { + assert.equal(BODY_INSET, 10); + assert.equal(RESIZE_HANDLE_SIZE, 12); + assert.equal(FOOTER_HANDLE_CLEARANCE, 22); + assert.equal(SCROLLBAR_WIDTH, 8); + }); + + test('the sizes a note is created and clamped at', function () { + assert.deepEqual(DEFAULT_SIZE, {width: 260, height: 180}); + assert.deepEqual(MIN_SIZE, {width: 84, height: 62}); + }); +}); diff --git a/blockly-workspace-notes/test/lock_permission.mocha.js b/blockly-workspace-notes/test/lock_permission.mocha.js new file mode 100644 index 0000000..476832b --- /dev/null +++ b/blockly-workspace-notes/test/lock_permission.mocha.js @@ -0,0 +1,103 @@ +/** + * @fileoverview Tests for who may lock and unlock a note. + * + * The context menu's own preconditions cannot be reached headlessly — they + * narrow on `instanceof Note`, and a Node test can only build a `NoteComment` + * — so the rule they consult is tested here directly instead. + */ + +import * as Blockly from 'blockly/core'; +import {assert} from 'chai'; + +import {NoteComment, WorkspaceNotes} from '../src/index'; +import { + canToggleLock, + clearLockPermission, + setLockPermission, +} from '../src/ui/lock_permission'; + +suite('Lock permission', function () { + setup(function () { + this.workspace = new Blockly.Workspace(); + }); + + teardown(function () { + clearLockPermission(this.workspace); + this.workspace.dispose(); + }); + + test('a workspace with no rule lets anyone lock', function () { + const note = new NoteComment(this.workspace); + assert.isTrue(canToggleLock(note)); + }); + + test('a rule that refuses is honoured', function () { + const note = new NoteComment(this.workspace); + setLockPermission(this.workspace, () => false); + assert.isFalse(canToggleLock(note)); + }); + + test('the rule is asked about the note in front of it', function () { + const plain = new NoteComment(this.workspace); + const titled = new NoteComment(this.workspace); + titled.setTitle('Brief'); + setLockPermission(this.workspace, (note) => !note.getTitle()); + assert.isTrue(canToggleLock(plain)); + assert.isFalse(canToggleLock(titled)); + }); + + test('clearing a rule restores the permissive default', function () { + const note = new NoteComment(this.workspace); + setLockPermission(this.workspace, () => false); + clearLockPermission(this.workspace); + assert.isTrue(canToggleLock(note)); + }); + + // The reason this is held per workspace rather than captured once at + // registration, the way the palette is: a host running an authoring + // workspace beside a restricted one must not have the first one's + // permissions decide for both. + test('a rule does not leak to another workspace', function () { + const other = new Blockly.Workspace(); + try { + setLockPermission(this.workspace, () => false); + assert.isFalse(canToggleLock(new NoteComment(this.workspace))); + assert.isTrue(canToggleLock(new NoteComment(other))); + } finally { + clearLockPermission(other); + other.dispose(); + } + }); + + suite('through the plugin', function () { + test('two instances keep their own rules', function () { + const other = new Blockly.Workspace(); + const mine = new WorkspaceNotes(this.workspace, { + contextMenu: false, + canToggleLock: () => false, + }); + const theirs = new WorkspaceNotes(other, {contextMenu: false}); + try { + mine.init(); + theirs.init(); + assert.isFalse(canToggleLock(new NoteComment(this.workspace))); + assert.isTrue(canToggleLock(new NoteComment(other))); + } finally { + mine.dispose(); + theirs.dispose(); + other.dispose(); + } + }); + + test('disposing forgets the rule', function () { + const plugin = new WorkspaceNotes(this.workspace, { + contextMenu: false, + canToggleLock: () => false, + }); + plugin.init(); + assert.isFalse(canToggleLock(new NoteComment(this.workspace))); + plugin.dispose(); + assert.isTrue(canToggleLock(new NoteComment(this.workspace))); + }); + }); +}); diff --git a/blockly-workspace-notes/test/note.mocha.js b/blockly-workspace-notes/test/note.mocha.js new file mode 100644 index 0000000..d43ee70 --- /dev/null +++ b/blockly-workspace-notes/test/note.mocha.js @@ -0,0 +1,311 @@ +/** + * @fileoverview Model tests for notes. + * + * These run headlessly: `blockly/core` resolves to `core-node.js` in Node, + * which provides jsdom only for XML handling, so `Blockly.inject` — and + * therefore any rendered note — is unavailable. NoteComment carries the same + * state as its rendered sibling, which is exactly what these cover. + */ + +import * as Blockly from 'blockly/core'; +import {assert} from 'chai'; + +import {DEFAULT_COLOUR, NoteChange, NoteComment, isNote} from '../src/index'; +import {nextZIndex, previousZIndex} from '../src/model/stacking'; +import {NOTE_BRAND} from '../src/constants/brand'; + +suite('Note model', function () { + setup(function () { + this.workspace = new Blockly.Workspace(); + }); + + teardown(function () { + this.workspace.dispose(); + }); + + suite('defaults', function () { + test('a new note has no title and the default colour', function () { + const note = new NoteComment(this.workspace); + assert.equal(note.getTitle(), ''); + assert.equal(note.getColour(), DEFAULT_COLOUR); + assert.isFalse(note.isPinned()); + assert.isFalse(note.isLocked()); + assert.equal(note.getZIndex(), 0); + }); + + test('a new note stamps its metadata', function () { + const note = new NoteComment(this.workspace); + const meta = note.getMeta(); + assert.equal(meta.author, ''); + assert.isNotEmpty(meta.createdAt); + assert.equal(meta.updatedAt, meta.createdAt); + }); + + test('it registers as a top comment like any other', function () { + const note = new NoteComment(this.workspace); + assert.deepEqual(this.workspace.getTopComments(false), [note]); + assert.equal(this.workspace.getCommentById(note.id), note); + }); + }); + + suite('accessors', function () { + setup(function () { + this.note = new NoteComment(this.workspace); + }); + + test('setTitle round-trips', function () { + this.note.setTitle('Refactor'); + assert.equal(this.note.getTitle(), 'Refactor'); + }); + + test('setColour normalizes through Blockly colour parsing', function () { + this.note.setColour('#ffd6a5'); + assert.equal(this.note.getColour(), '#ffd6a5'); + }); + + test('an unparseable colour falls back to the default', function () { + this.note.setColour('not-a-colour'); + assert.equal(this.note.getColour(), DEFAULT_COLOUR); + }); + + test('setZIndex coerces to a number', function () { + this.note.setZIndex('4'); + assert.strictEqual(this.note.getZIndex(), 4); + }); + + test('editing bumps updatedAt but not createdAt', function () { + const before = this.note.getMeta(); + // Timestamps have millisecond resolution, so force a distinct one. + this.note.getNoteState().meta.updatedAt = '1970-01-01T00:00:00.000Z'; + this.note.setTitle('Changed'); + const after = this.note.getMeta(); + assert.equal(after.createdAt, before.createdAt); + assert.notEqual(after.updatedAt, '1970-01-01T00:00:00.000Z'); + }); + + test('restoreMeta does not bump updatedAt', function () { + this.note.restoreMeta({ + author: 'ada', + createdAt: '2020-01-01T00:00:00.000Z', + updatedAt: '2020-01-02T00:00:00.000Z', + }); + assert.deepEqual(this.note.getMeta(), { + author: 'ada', + createdAt: '2020-01-01T00:00:00.000Z', + updatedAt: '2020-01-02T00:00:00.000Z', + }); + }); + }); + + suite('metadata', function () { + test('a body edit is recorded as a change', function () { + const note = new NoteComment(this.workspace); + const before = note.getMeta().updatedAt; + note.restoreMeta({updatedAt: '2000-01-01T00:00:00.000Z'}); + + note.setText('written just now'); + + assert.notEqual( + note.getMeta().updatedAt, + '2000-01-01T00:00:00.000Z', + 'writing in a note should count as changing it', + ); + assert.isString(before); + }); + + test('setting the same text again changes nothing', function () { + const note = new NoteComment(this.workspace); + note.setText('same'); + note.restoreMeta({updatedAt: '2000-01-01T00:00:00.000Z'}); + + note.setText('same'); + + assert.equal(note.getMeta().updatedAt, '2000-01-01T00:00:00.000Z'); + }); + + test('restoring metadata does not stamp a new time', function () { + const note = new NoteComment(this.workspace); + note.restoreMeta({ + author: 'ada', + createdAt: '2020-05-05T00:00:00.000Z', + updatedAt: '2020-05-05T00:00:00.000Z', + }); + + assert.equal(note.getMeta().author, 'ada'); + assert.equal(note.getMeta().updatedAt, '2020-05-05T00:00:00.000Z'); + }); + }); + + suite('pinning', function () { + test('pinning holds the note in place', function () { + const note = new NoteComment(this.workspace); + assert.isTrue(note.isOwnMovable()); + note.setPinned(true); + assert.isTrue(note.isPinned()); + assert.isFalse(note.isOwnMovable()); + }); + + test('unpinning releases it again', function () { + const note = new NoteComment(this.workspace); + note.setPinned(true); + note.setPinned(false); + assert.isFalse(note.isPinned()); + assert.isTrue(note.isOwnMovable()); + }); + }); + + // `isNote` reads this mark instead of testing instanceof, and the mixin's + // `as new (...) => ...` cast means the brand reaches no .d.ts at all - so + // nothing type-checks that it is still there. Were it dropped, `isNote` + // would return false for everything and the serializer would write an empty + // payload without throwing. + suite('the note brand', function () { + test('every note carries it', function () { + assert.isTrue(new NoteComment(this.workspace)[NOTE_BRAND]); + assert.isFalse(isNote({title: 'not a note'})); + assert.isFalse(isNote(null)); + }); + + test('it stays a prototype accessor, not an own field', function () { + const note = new NoteComment(this.workspace); + assert.notProperty( + Object.getOwnPropertyDescriptors(note), + NOTE_BRAND, + 'an own field would be enumerable, and would land after super()', + ); + const mixin = Object.getPrototypeOf(NoteComment.prototype); + const descriptor = Object.getOwnPropertyDescriptor(mixin, NOTE_BRAND); + assert.isFunction(descriptor?.get, 'the brand must be a getter'); + }); + }); + + suite('locking', function () { + test('locking makes the note read-only and undeletable', function () { + const note = new NoteComment(this.workspace); + assert.isTrue(note.isOwnEditable()); + assert.isTrue(note.isOwnDeletable()); + note.setLocked(true); + assert.isTrue(note.isLocked()); + assert.isFalse(note.isOwnEditable()); + assert.isFalse(note.isOwnDeletable()); + }); + + test('unlocking restores both', function () { + const note = new NoteComment(this.workspace); + note.setLocked(true); + note.setLocked(false); + assert.isFalse(note.isLocked()); + assert.isTrue(note.isOwnEditable()); + assert.isTrue(note.isOwnDeletable()); + }); + + // Locking owns editable and deletable, pinning owns movable, and neither + // writes the other's. Without this the two can silently merge, and + // unpinning a locked note would hand its movement back. + test('locking never touches movability', function () { + const note = new NoteComment(this.workspace); + note.setLocked(true); + assert.isTrue(note.isOwnMovable()); + note.setLocked(false); + assert.isTrue(note.isOwnMovable()); + }); + + test('locking and pinning compose', function () { + const note = new NoteComment(this.workspace); + note.setPinned(true); + note.setLocked(true); + assert.isFalse(note.isOwnMovable()); + assert.isFalse(note.isOwnEditable()); + assert.isFalse(note.isOwnDeletable()); + + note.setLocked(false); + assert.isTrue(note.isPinned()); + assert.isFalse(note.isOwnMovable()); + assert.isTrue(note.isOwnEditable()); + assert.isTrue(note.isOwnDeletable()); + }); + + test('saveNoteState carries the flag', function () { + const note = new NoteComment(this.workspace); + note.setLocked(true); + assert.isTrue(note.saveNoteState().locked); + }); + }); + + suite('stacking helpers', function () { + test('nextZIndex is one past the highest in use', function () { + assert.equal(nextZIndex(this.workspace), 1); + const note = new NoteComment(this.workspace); + note.setZIndex(7); + assert.equal(nextZIndex(this.workspace), 8); + }); + + test('previousZIndex is one below the lowest in use', function () { + const note = new NoteComment(this.workspace); + note.setZIndex(-2); + assert.equal(previousZIndex(this.workspace), -3); + }); + }); + + suite('applyNoteProperty', function () { + setup(function () { + this.note = new NoteComment(this.workspace); + }); + + test("'*' applies a whole state object", function () { + this.note.applyNoteProperty('*', { + title: 'All', + colour: '#c7e4ff', + pinned: true, + locked: true, + zIndex: 3, + meta: {author: 'x', createdAt: 'a', updatedAt: 'b'}, + }); + assert.equal(this.note.getTitle(), 'All'); + assert.equal(this.note.getColour(), '#c7e4ff'); + assert.isTrue(this.note.isPinned()); + assert.isTrue(this.note.isLocked()); + assert.equal(this.note.getZIndex(), 3); + assert.equal(this.note.getMeta().author, 'x'); + }); + + // A state object written before locking existed has no `locked` key at + // all. It has to read as false, not undefined, or it survives into + // saveNoteState and the change comparison stops seeing a real lock. + test("'*' without a locked key reads as unlocked", function () { + this.note.setLocked(true); + this.note.applyNoteProperty('*', { + title: '', + colour: DEFAULT_COLOUR, + pinned: false, + zIndex: 0, + meta: {author: '', createdAt: 'a', updatedAt: 'b'}, + }); + assert.isFalse(this.note.isLocked()); + assert.isFalse(this.note.saveNoteState().locked); + }); + + test("'*' with null is a no-op, so a delete snapshot replays", function () { + this.note.setTitle('Keep me'); + this.note.applyNoteProperty('*', null); + assert.equal(this.note.getTitle(), 'Keep me'); + }); + + test('it does not fire an event, unlike the setters', function () { + const fired = []; + this.workspace.addChangeListener((e) => fired.push(e)); + this.note.applyNoteProperty('title', 'Silent'); + assert.isEmpty(fired.filter((e) => e instanceof NoteChange)); + }); + }); + + suite('isNote', function () { + test('recognizes notes and rejects plain comments', function () { + assert.isTrue(isNote(new NoteComment(this.workspace))); + assert.isFalse( + isNote(new Blockly.comments.WorkspaceComment(this.workspace)), + ); + assert.isFalse(isNote(null)); + }); + }); +}); diff --git a/blockly-workspace-notes/test/plugin.mocha.js b/blockly-workspace-notes/test/plugin.mocha.js new file mode 100644 index 0000000..d6d3c28 --- /dev/null +++ b/blockly-workspace-notes/test/plugin.mocha.js @@ -0,0 +1,128 @@ +/** + * @fileoverview Lifecycle tests: what `init` replaces, and whether `dispose` + * gives all of it back. + * + * The registries have their own coverage in the suites that use them. What is + * checked here is the state that is *not* a registry — the statics and + * instance properties the plugin writes on core — because nothing else would + * notice if one of them were left behind. + */ + +import * as Blockly from 'blockly/core'; +import {assert} from 'chai'; + +import {WorkspaceNotes} from '../src/index'; + +suite('Plugin lifecycle', function () { + setup(function () { + this.workspace = new Blockly.Workspace(); + }); + + teardown(function () { + this.workspace.dispose(); + }); + + suite('defaultCommentSize', function () { + test('init applies the configured default size', function () { + const plugin = new WorkspaceNotes(this.workspace, { + contextMenu: false, + defaultSize: {width: 300, height: 200}, + }); + plugin.init(); + try { + const size = Blockly.comments.CommentView.defaultCommentSize; + assert.equal(size.width, 300); + assert.equal(size.height, 200); + } finally { + plugin.dispose(); + } + }); + + test('dispose puts core’s own default back', function () { + const original = Blockly.comments.CommentView.defaultCommentSize; + + const plugin = new WorkspaceNotes(this.workspace, {contextMenu: false}); + plugin.init(); + assert.notEqual( + Blockly.comments.CommentView.defaultCommentSize, + original, + 'init should have replaced the default', + ); + + plugin.dispose(); + assert.strictEqual( + Blockly.comments.CommentView.defaultCommentSize, + original, + 'a disposed plugin must not leave core resized', + ); + }); + + test('a live plugin keeps its size when another is disposed', function () { + // The size is a static shared by every workspace on the page, so the + // swap is reference-counted: disposing one plugin must not resize the + // notes of another that is still running. + const original = Blockly.comments.CommentView.defaultCommentSize; + + const first = new WorkspaceNotes(this.workspace, { + contextMenu: false, + defaultSize: {width: 300, height: 200}, + }); + const other = new Blockly.Workspace(); + const second = new WorkspaceNotes(other, { + contextMenu: false, + defaultSize: {width: 400, height: 250}, + }); + + first.init(); + second.init(); + try { + first.dispose(); + const size = Blockly.comments.CommentView.defaultCommentSize; + assert.equal(size.width, 300, 'the live plugin still needs a size'); + assert.equal(size.height, 200); + + second.dispose(); + assert.strictEqual( + Blockly.comments.CommentView.defaultCommentSize, + original, + 'the last one out restores core', + ); + } finally { + other.dispose(); + } + }); + + test('disposing twice does not restore over a live plugin', function () { + const first = new WorkspaceNotes(this.workspace, {contextMenu: false}); + first.init(); + first.dispose(); + + const second = new WorkspaceNotes(this.workspace, { + contextMenu: false, + defaultSize: {width: 400, height: 250}, + }); + second.init(); + try { + first.dispose(); + const size = Blockly.comments.CommentView.defaultCommentSize; + assert.equal(size.width, 400); + assert.equal(size.height, 250); + } finally { + second.dispose(); + } + }); + }); + + suite('newComment', function () { + test('dispose restores the workspace’s own factory', function () { + const original = this.workspace.newComment; + + const plugin = new WorkspaceNotes(this.workspace, {contextMenu: false}); + plugin.init(); + assert.notEqual(this.workspace.newComment, original); + + plugin.dispose(); + assert.strictEqual(this.workspace.newComment, original); + }); + }); +}); diff --git a/blockly-workspace-notes/test/serializer.mocha.js b/blockly-workspace-notes/test/serializer.mocha.js new file mode 100644 index 0000000..9e3f9e3 --- /dev/null +++ b/blockly-workspace-notes/test/serializer.mocha.js @@ -0,0 +1,535 @@ +/** + * @fileoverview Serialization tests: round-tripping, the sparse JSON shape, + * legacy files, migrations, and undo fidelity for note-specific fields. + */ + +import * as Blockly from 'blockly/core'; +import {assert} from 'chai'; + +import { + DEFAULT_COLOUR, + NOTE_SERIALIZER_NAME, + NoteComment, + SCHEMA_VERSION, + WorkspaceNotes, + appendNote, + isNote, + migrate, + saveNote, +} from '../src/index'; + +/** + * Blockly queues events and flushes them on a later macrotask, so the undo + * stack is not populated synchronously. Await this before inspecting it. + * + * @returns {!Promise} Resolves once the queue has drained. + */ +function flushEvents() { + return new Promise((resolve) => setTimeout(resolve, 0)).then( + () => new Promise((resolve) => setTimeout(resolve, 0)), + ); +} + +suite('Note serialization', function () { + setup(function () { + this.workspace = new Blockly.Workspace(); + this.plugin = new WorkspaceNotes(this.workspace, {contextMenu: false}); + this.plugin.init(); + }); + + teardown(function () { + this.plugin.dispose(); + this.workspace.dispose(); + }); + + /** + * @param {!Blockly.Workspace} workspace The workspace to add the note to. + * @param {!object} [overrides] Fields to set on the new note. + * @returns {!NoteComment} A note on the test workspace. + */ + function makeNote(workspace, overrides = {}) { + const note = new NoteComment(workspace); + if (overrides.text) note.setText(overrides.text); + if (overrides.title) note.setTitle(overrides.title); + if (overrides.colour) note.setColour(overrides.colour); + if (overrides.zIndex) note.setZIndex(overrides.zIndex); + if (overrides.pinned) note.setPinned(true); + if (overrides.locked) note.setLocked(true); + if (overrides.collapsed) note.setCollapsed(true); + note.moveTo( + new Blockly.utils.Coordinate(overrides.x ?? 0, overrides.y ?? 0), + ); + note.setSize( + new Blockly.utils.Size(overrides.width ?? 200, overrides.height ?? 120), + ); + return note; + } + + suite('save', function () { + test('an empty workspace writes no notes key at all', function () { + const state = Blockly.serialization.workspaces.save(this.workspace); + assert.notProperty(state, NOTE_SERIALIZER_NAME); + }); + + test('notes are written under a versioned envelope', function () { + makeNote(this.workspace, {text: 'hello'}); + const state = Blockly.serialization.workspaces.save(this.workspace); + assert.equal(state[NOTE_SERIALIZER_NAME].version, SCHEMA_VERSION); + assert.lengthOf(state[NOTE_SERIALIZER_NAME].notes, 1); + }); + + test('a default note omits every optional field', function () { + const note = makeNote(this.workspace, {x: 10, y: 20}); + const saved = saveNote(note, {addCoordinates: true, saveIds: true}); + + assert.deepEqual( + { + id: saved.id, + x: saved.x, + y: saved.y, + width: saved.width, + height: saved.height, + }, + {id: note.id, x: 10, y: 20, width: 200, height: 120}, + ); + for (const key of [ + 'text', + 'title', + 'colour', + 'collapsed', + 'pinned', + 'locked', + 'zIndex', + 'editable', + 'movable', + 'deletable', + ]) { + assert.notProperty(saved, key, `expected ${key} to be omitted`); + } + }); + + test('non-default flags are written, defaults are not', function () { + const note = makeNote(this.workspace, { + title: 'TODO', + colour: '#ffd6a5', + collapsed: true, + zIndex: 3, + }); + note.setEditable(false); + const saved = saveNote(note, {saveIds: true}); + + assert.equal(saved.title, 'TODO'); + assert.equal(saved.colour, '#ffd6a5'); + assert.isTrue(saved.collapsed); + assert.equal(saved.zIndex, 3); + assert.isFalse(saved.editable); + // `deletable` is still at its default, so it stays out of the file. + assert.notProperty(saved, 'deletable'); + }); + + test('a pinned note omits the movable flag it implies', function () { + const note = makeNote(this.workspace, {pinned: true}); + const saved = saveNote(note, {saveIds: true}); + assert.isTrue(saved.pinned); + assert.notProperty(saved, 'movable'); + }); + + test('a locked note omits the two flags it implies', function () { + const note = makeNote(this.workspace, {locked: true}); + const saved = saveNote(note, {saveIds: true}); + assert.isTrue(saved.locked); + assert.notProperty(saved, 'editable'); + assert.notProperty(saved, 'deletable'); + }); + + test('the default colour is treated as "no colour"', function () { + const note = makeNote(this.workspace); + note.setColour(DEFAULT_COLOUR); + assert.notProperty(saveNote(note), 'colour'); + }); + }); + + suite('no duplication', function () { + test('notes are not also written under workspaceComments', function () { + makeNote(this.workspace, {text: 'only once'}); + const state = Blockly.serialization.workspaces.save(this.workspace); + assert.notProperty( + state, + 'workspaceComments', + 'the built-in comment serializer must not write notes a second time', + ); + }); + + test('a save/load cycle does not multiply notes', function () { + makeNote(this.workspace, {text: 'a'}); + makeNote(this.workspace, {text: 'b'}); + const state = Blockly.serialization.workspaces.save(this.workspace); + + const target = new Blockly.Workspace(); + try { + Blockly.serialization.workspaces.load(state, target); + assert.lengthOf(target.getTopComments(false), 2); + // And loading the same file again replaces rather than appends. + Blockly.serialization.workspaces.load(state, target); + assert.lengthOf(target.getTopComments(false), 2); + } finally { + target.dispose(); + } + }); + }); + + suite('round trip', function () { + test('every field survives save, load and save again', function () { + makeNote(this.workspace, { + text: 'Check this loop', + title: 'TODO', + colour: '#c7e4ff', + zIndex: 2, + x: 40, + y: 20, + width: 300, + height: 160, + }); + makeNote(this.workspace, { + text: 'Held in place', + pinned: true, + collapsed: true, + x: 400, + y: 20, + }); + makeNote(this.workspace, { + text: 'Read only', + title: 'Brief', + locked: true, + x: 400, + y: 220, + }); + + const first = Blockly.serialization.workspaces.save(this.workspace); + + const target = new Blockly.Workspace(); + try { + Blockly.serialization.workspaces.load(first, target); + const second = Blockly.serialization.workspaces.save(target); + assert.deepEqual(second, first); + } finally { + target.dispose(); + } + }); + + test('loaded notes are notes, not plain comments', function () { + makeNote(this.workspace, {title: 'T'}); + const state = Blockly.serialization.workspaces.save(this.workspace); + + const target = new Blockly.Workspace(); + try { + Blockly.serialization.workspaces.load(state, target); + const [loaded] = target.getTopComments(false); + assert.isTrue(isNote(loaded)); + assert.equal(loaded.getTitle(), 'T'); + } finally { + target.dispose(); + } + }); + + test('metadata is restored verbatim rather than re-stamped', function () { + const note = makeNote(this.workspace); + note.restoreMeta({ + author: 'ada', + createdAt: '2020-01-01T00:00:00.000Z', + updatedAt: '2020-01-02T00:00:00.000Z', + }); + const state = Blockly.serialization.workspaces.save(this.workspace); + + const target = new Blockly.Workspace(); + try { + Blockly.serialization.workspaces.load(state, target); + assert.deepEqual(target.getTopComments(false)[0].getMeta(), { + author: 'ada', + createdAt: '2020-01-01T00:00:00.000Z', + updatedAt: '2020-01-02T00:00:00.000Z', + }); + } finally { + target.dispose(); + } + }); + }); + + suite('legacy files', function () { + test('a workspaceComments-only file loads as notes', function () { + Blockly.serialization.workspaces.load( + { + workspaceComments: [ + {id: 'legacy1', x: 5, y: 6, width: 100, height: 50, text: 'old'}, + ], + }, + this.workspace, + ); + + const loaded = this.workspace.getCommentById('legacy1'); + assert.isTrue(isNote(loaded)); + assert.equal(loaded.getText(), 'old'); + assert.equal(loaded.getColour(), DEFAULT_COLOUR); + }); + + test('a legacy file is re-saved in the new format', function () { + Blockly.serialization.workspaces.load( + {workspaceComments: [{id: 'legacy1', width: 100, height: 50}]}, + this.workspace, + ); + const state = Blockly.serialization.workspaces.save(this.workspace); + assert.notProperty(state, 'workspaceComments'); + assert.lengthOf(state[NOTE_SERIALIZER_NAME].notes, 1); + }); + + test('a loaded note wins over a duplicate legacy entry', function () { + Blockly.serialization.workspaces.load( + { + workspaceNotes: { + version: SCHEMA_VERSION, + notes: [{id: 'dup', width: 100, height: 50, title: 'new'}], + }, + workspaceComments: [{id: 'dup', width: 100, height: 50}], + }, + this.workspace, + ); + + assert.lengthOf(this.workspace.getTopComments(false), 1); + assert.equal(this.workspace.getCommentById('dup').getTitle(), 'new'); + }); + }); + + suite('migrations', function () { + test('a bare array is treated as the unversioned shape', function () { + const migrated = migrate([{id: 'a', width: 1, height: 2}]); + assert.equal(migrated.version, SCHEMA_VERSION); + assert.lengthOf(migrated.notes, 1); + }); + + test('an explicit v0 envelope upgrades', function () { + const migrated = migrate({version: 0, notes: [{id: 'a'}]}); + assert.equal(migrated.version, SCHEMA_VERSION); + assert.lengthOf(migrated.notes, 1); + }); + + test('a current payload passes through unchanged', function () { + const state = {version: SCHEMA_VERSION, notes: [{id: 'a'}]}; + assert.deepEqual(migrate(state), state); + }); + + test('a newer payload keeps its notes rather than throwing', function () { + const migrated = migrate({version: 99, notes: [{id: 'a'}]}); + assert.equal(migrated.version, SCHEMA_VERSION); + assert.lengthOf(migrated.notes, 1); + }); + + test('an unversioned array loads end to end', function () { + Blockly.serialization.workspaces.load( + {workspaceNotes: [{id: 'bare', width: 100, height: 50, title: 'B'}]}, + this.workspace, + ); + assert.equal(this.workspace.getCommentById('bare').getTitle(), 'B'); + }); + }); + + suite('partial state', function () { + // A sparse file is the whole point of the format, so loading one must + // never invent a value. Core falls back to the object's own geometry; + // falling back to zero would leave a note that is saved but invisible. + test('a missing height keeps the note’s own height', function () { + const note = appendNote({width: 300}, this.workspace); + assert.equal(note.getSize().width, 300); + assert.isAbove(note.getSize().height, 0, 'height must not collapse'); + }); + + test('a missing width keeps the note’s own width', function () { + const note = appendNote({height: 90}, this.workspace); + assert.equal(note.getSize().height, 90); + assert.isAbove(note.getSize().width, 0, 'width must not collapse'); + }); + + test('a locked note loads locked', function () { + const note = appendNote({locked: true}, this.workspace); + assert.isTrue(note.isLocked()); + assert.isFalse(note.isOwnEditable()); + assert.isFalse(note.isOwnDeletable()); + }); + + test('an absent coordinate is not RTL-flipped', function () { + // The flip converts a saved x into a workspace x, so it only applies to + // a value the file actually carried. Applying it to the fallback sent + // the note to the far edge of an RTL workspace instead of leaving it + // where it was. + const rtl = new Blockly.Workspace(new Blockly.Options({rtl: true})); + // Headless workspaces report a width of 0, which hides the arithmetic. + rtl.getWidth = () => 500; + try { + const note = appendNote({y: 20}, rtl); + const loc = note.getRelativeToSurfaceXY(); + assert.equal(loc.x, 0, 'x should not have moved'); + assert.equal(loc.y, 20); + } finally { + rtl.dispose(); + } + }); + + test('a coordinate that is present is still RTL-flipped', function () { + const rtl = new Blockly.Workspace(new Blockly.Options({rtl: true})); + rtl.getWidth = () => 500; + try { + const note = appendNote({x: 120, y: 20}, rtl); + assert.equal(note.getRelativeToSurfaceXY().x, 380); + } finally { + rtl.dispose(); + } + }); + + test('no geometry at all leaves the defaults untouched', function () { + const note = appendNote({title: 'Bare'}, this.workspace); + assert.equal(note.getTitle(), 'Bare'); + assert.isAbove(note.getSize().width, 0); + assert.isAbove(note.getSize().height, 0); + }); + + test('an unknown key is ignored rather than throwing', function () { + const note = appendNote( + {width: 200, height: 120, somethingNewer: 42}, + this.workspace, + ); + assert.isTrue(isNote(note)); + }); + }); + + suite('saveNote defaults', function () { + // The signature mirrors `Blockly.serialization.workspaceComments.save`, + // whose `saveIds` defaults to true. A different default here would lose + // ids for anyone porting a call across. + test('ids are written unless asked otherwise', function () { + const note = makeNote(this.workspace); + assert.equal(saveNote(note).id, note.id); + assert.isUndefined(saveNote(note, {saveIds: false}).id); + }); + + test('coordinates are omitted unless asked for', function () { + const note = makeNote(this.workspace, {x: 10, y: 20}); + assert.isUndefined(saveNote(note).x); + assert.equal(saveNote(note, {addCoordinates: true}).x, 10); + }); + + test('the state carries no inherited properties', function () { + const state = saveNote(makeNote(this.workspace)); + assert.isNull(Object.getPrototypeOf(state)); + }); + }); + + suite('clear', function () { + test('loading disposes the notes already present', function () { + makeNote(this.workspace, {text: 'gone'}); + Blockly.serialization.workspaces.load( + {workspaceNotes: {version: SCHEMA_VERSION, notes: []}}, + this.workspace, + ); + assert.isEmpty(this.workspace.getTopComments(false)); + }); + + test('plain comments are cleared too, so they cannot pile up', function () { + new Blockly.comments.WorkspaceComment(this.workspace); + Blockly.serialization.workspaces.load({}, this.workspace); + assert.isEmpty(this.workspace.getTopComments(false)); + }); + }); + + suite('undo', function () { + test('undoing a delete restores the title and colour', async function () { + const note = makeNote(this.workspace, { + title: 'Important', + colour: '#ffd6a5', + }); + const id = note.id; + await flushEvents(); + + note.dispose(); + await flushEvents(); + assert.isNull(this.workspace.getCommentById(id)); + + this.workspace.undo(false); + await flushEvents(); + + const restored = this.workspace.getCommentById(id); + assert.isNotNull(restored, 'the note should come back'); + assert.equal(restored.getTitle(), 'Important'); + assert.equal(restored.getColour(), '#ffd6a5'); + }); + + test('undoing a title change reverts it', async function () { + const note = makeNote(this.workspace, {title: 'Before'}); + await flushEvents(); + + note.setTitle('After'); + await flushEvents(); + assert.equal(note.getTitle(), 'After'); + + // One event, so one undo. The inline title editor wraps exactly this + // call in an event group, so renaming is a single step there too. + this.workspace.undo(false); + await flushEvents(); + assert.equal(note.getTitle(), 'Before'); + }); + + test('undoing a lock releases the note again', async function () { + const note = makeNote(this.workspace); + await flushEvents(); + + note.setLocked(true); + await flushEvents(); + assert.isFalse(note.isOwnEditable()); + assert.isFalse(note.isOwnDeletable()); + + this.workspace.undo(false); + await flushEvents(); + assert.isFalse(note.isLocked()); + assert.isTrue(note.isOwnEditable()); + assert.isTrue(note.isOwnDeletable()); + }); + + test('undoing a colour change reverts it', async function () { + const note = makeNote(this.workspace); + await flushEvents(); + + note.setColour('#c9efc2'); + await flushEvents(); + assert.equal(note.getColour(), '#c9efc2'); + + this.workspace.undo(false); + await flushEvents(); + assert.equal(note.getColour(), DEFAULT_COLOUR); + + this.workspace.redo(); + await flushEvents(); + assert.equal(note.getColour(), '#c9efc2'); + }); + + test('a no-op change never reaches the undo stack', async function () { + const note = makeNote(this.workspace, {title: 'Same'}); + await flushEvents(); + const depth = this.workspace.getUndoStack().length; + + note.setTitle('Same'); + await flushEvents(); + assert.equal(this.workspace.getUndoStack().length, depth); + }); + + test('loading a file does not fill the undo stack', async function () { + const state = { + workspaceNotes: { + version: SCHEMA_VERSION, + notes: [{id: 'a', width: 100, height: 50, title: 'A'}], + }, + }; + this.workspace.clearUndo(); + Blockly.serialization.workspaces.load(state, this.workspace); + await flushEvents(); + assert.isEmpty(this.workspace.getUndoStack()); + }); + }); +}); diff --git a/blockly-workspace-notes/test/xml.mocha.js b/blockly-workspace-notes/test/xml.mocha.js new file mode 100644 index 0000000..7e6a7d7 --- /dev/null +++ b/blockly-workspace-notes/test/xml.mocha.js @@ -0,0 +1,435 @@ +/** + * @fileoverview XML serialization tests. + * + * Unlike the rendered chrome, `Blockly.Xml` works headlessly — `core-node.js` + * injects jsdom precisely so XML handling works in Node — so the whole format + * is covered here. + */ + +import * as Blockly from 'blockly/core'; +import {assert} from 'chai'; + +import { + DEFAULT_COLOUR, + NoteComment, + WorkspaceNotes, + domToNoteState, + isNote, + noteToDom, +} from '../src/index'; + +/** + * @param {!Element} dom An `` element. + * @returns {!Array} Its top-level `` children. + */ +function comments(dom) { + return Array.from(dom.childNodes).filter( + (node) => node.nodeName.toLowerCase() === 'comment', + ); +} + +suite('Note XML serialization', function () { + setup(function () { + this.workspace = new Blockly.Workspace(); + this.plugin = new WorkspaceNotes(this.workspace, {contextMenu: false}); + this.plugin.init(); + }); + + teardown(function () { + this.plugin.dispose(); + this.workspace.dispose(); + }); + + /** + * @param {!Blockly.Workspace} workspace The workspace to add to. + * @param {!object} [overrides] Fields to set on the note. + * @returns {!NoteComment} The new note. + */ + function makeNote(workspace, overrides = {}) { + const note = new NoteComment(workspace); + if (overrides.locked) note.setLocked(true); + if (overrides.text) note.setText(overrides.text); + if (overrides.title) note.setTitle(overrides.title); + if (overrides.colour) note.setColour(overrides.colour); + if (overrides.zIndex) note.setZIndex(overrides.zIndex); + if (overrides.pinned) note.setPinned(true); + if (overrides.collapsed) note.setCollapsed(true); + note.moveTo( + new Blockly.utils.Coordinate(overrides.x ?? 0, overrides.y ?? 0), + ); + note.setSize( + new Blockly.utils.Size(overrides.width ?? 200, overrides.height ?? 120), + ); + return note; + } + + suite('writing', function () { + test('note fields are written as attributes on ', function () { + makeNote(this.workspace, { + text: 'body', + title: 'TODO', + colour: '#ffd6a5', + zIndex: 3, + x: 40, + y: 20, + }); + + const [elem] = comments(Blockly.Xml.workspaceToDom(this.workspace)); + assert.equal(elem.getAttribute('title'), 'TODO'); + assert.equal(elem.getAttribute('colour'), '#ffd6a5'); + assert.equal(elem.getAttribute('z'), '3'); + assert.equal(elem.textContent, 'body'); + }); + + test("core's own attributes are left untouched", function () { + makeNote(this.workspace, {x: 40, y: 20, width: 250, height: 130}); + const [elem] = comments(Blockly.Xml.workspaceToDom(this.workspace)); + assert.equal(elem.getAttribute('x'), '40'); + assert.equal(elem.getAttribute('y'), '20'); + assert.equal(elem.getAttribute('w'), '250'); + assert.equal(elem.getAttribute('h'), '130'); + assert.isNotNull(elem.getAttribute('id')); + }); + + test('a plain note writes no note-specific attributes', function () { + makeNote(this.workspace); + const [elem] = comments(Blockly.Xml.workspaceToDom(this.workspace)); + for (const name of ['title', 'colour', 'pinned', 'locked', 'z']) { + assert.isNull( + elem.getAttribute(name), + `expected ${name} to be omitted`, + ); + } + }); + + test('skipId omits the id, as it does for a plain comment', function () { + makeNote(this.workspace, {title: 'T'}); + const [elem] = comments(Blockly.Xml.workspaceToDom(this.workspace, true)); + assert.isNull(elem.getAttribute('id')); + assert.equal(elem.getAttribute('title'), 'T'); + }); + + test('attributes line up when several notes are saved', function () { + makeNote(this.workspace, {title: 'first', colour: '#ffd6a5'}); + makeNote(this.workspace, {title: 'second', colour: '#c7e4ff'}); + makeNote(this.workspace, {title: 'third'}); + + const elems = comments(Blockly.Xml.workspaceToDom(this.workspace)); + assert.deepEqual( + elems.map((e) => e.getAttribute('title')), + ['first', 'second', 'third'], + ); + assert.deepEqual( + elems.map((e) => e.getAttribute('colour')), + ['#ffd6a5', '#c7e4ff', null], + ); + }); + + test('metadata is written', function () { + const note = makeNote(this.workspace); + note.restoreMeta({ + author: 'ada', + createdAt: '2020-01-01T00:00:00.000Z', + updatedAt: '2020-01-02T00:00:00.000Z', + }); + const [elem] = comments(Blockly.Xml.workspaceToDom(this.workspace)); + assert.equal(elem.getAttribute('author'), 'ada'); + assert.equal(elem.getAttribute('created'), '2020-01-01T00:00:00.000Z'); + assert.equal(elem.getAttribute('updated'), '2020-01-02T00:00:00.000Z'); + }); + }); + + suite('reading', function () { + test('a decorated loads as a note', function () { + const dom = Blockly.utils.xml.textToDom( + 'body', + ); + Blockly.Xml.domToWorkspace(dom, this.workspace); + + const note = this.workspace.getCommentById('n1'); + assert.isTrue(isNote(note), 'should be a note, not a plain comment'); + assert.equal(note.getTitle(), 'TODO'); + assert.equal(note.getColour(), '#ffd6a5'); + assert.equal(note.getZIndex(), 2); + assert.equal(note.getText(), 'body'); + assert.equal(note.getSize().width, 250); + }); + + test('a pinned note comes back held in place', function () { + const dom = Blockly.utils.xml.textToDom( + '', + ); + Blockly.Xml.domToWorkspace(dom, this.workspace); + const note = this.workspace.getCommentById('n1'); + assert.isTrue(note.isPinned()); + assert.isFalse(note.isOwnMovable()); + }); + + test('a locked note comes back read-only and undeletable', function () { + const dom = Blockly.utils.xml.textToDom( + '', + ); + Blockly.Xml.domToWorkspace(dom, this.workspace); + const note = this.workspace.getCommentById('n1'); + assert.isTrue(note.isLocked()); + assert.isFalse(note.isOwnEditable()); + assert.isFalse(note.isOwnDeletable()); + }); + + test('a plain old still loads, with defaults', function () { + const dom = Blockly.utils.xml.textToDom( + 'legacy' + + '', + ); + Blockly.Xml.domToWorkspace(dom, this.workspace); + + const note = this.workspace.getCommentById('old'); + assert.isTrue(isNote(note)); + assert.equal(note.getText(), 'legacy'); + assert.equal(note.getColour(), DEFAULT_COLOUR); + assert.equal(note.getTitle(), ''); + }); + + test('blocks alongside comments still load, and are returned', function () { + Blockly.defineBlocksWithJsonArray([ + {type: 'xml_test_block', message0: 'test'}, + ]); + try { + const dom = Blockly.utils.xml.textToDom( + '' + + '', + ); + const ids = Blockly.Xml.domToWorkspace(dom, this.workspace); + assert.deepEqual(ids, ['b1']); + assert.equal(this.workspace.getCommentById('n1').getTitle(), 'T'); + assert.isNotNull(this.workspace.getBlockById('b1')); + } finally { + delete Blockly.Blocks['xml_test_block']; + } + }); + + test('the caller’s DOM is not mutated', function () { + const dom = Blockly.utils.xml.textToDom( + '', + ); + Blockly.Xml.domToWorkspace(dom, this.workspace); + assert.lengthOf( + comments(dom), + 1, + 'the comment element must survive so the DOM can be loaded again', + ); + + // And loading the same DOM a second time still works. + const second = new Blockly.Workspace(); + try { + Blockly.Xml.domToWorkspace(dom, second); + assert.equal(second.getCommentById('n1').getTitle(), 'T'); + } finally { + second.dispose(); + } + }); + + test('appendDomToWorkspace also produces notes', function () { + const dom = Blockly.utils.xml.textToDom( + '', + ); + // Blockly reaches domToWorkspace through a module-local binding here, + // so this only works because the entry point is wrapped separately. + Blockly.Xml.appendDomToWorkspace(dom, this.workspace); + assert.equal(this.workspace.getCommentById('n1').getTitle(), 'Appended'); + }); + }); + + suite('round trip', function () { + test('every field survives a save and load', function () { + makeNote(this.workspace, { + text: 'Check this loop', + title: 'TODO', + colour: '#c7e4ff', + zIndex: 2, + x: 40, + y: 20, + width: 300, + height: 160, + }); + makeNote(this.workspace, { + text: 'Locked', + pinned: true, + collapsed: true, + x: 400, + y: 20, + }); + + const first = Blockly.Xml.domToText( + Blockly.Xml.workspaceToDom(this.workspace), + ); + + const target = new Blockly.Workspace(); + const plugin = new WorkspaceNotes(target, {contextMenu: false}); + plugin.init(); + try { + Blockly.Xml.domToWorkspace(Blockly.utils.xml.textToDom(first), target); + const second = Blockly.Xml.domToText( + Blockly.Xml.workspaceToDom(target), + ); + assert.equal(second, first); + } finally { + plugin.dispose(); + target.dispose(); + } + }); + + test('XML and JSON agree on the same workspace', function () { + makeNote(this.workspace, { + text: 'body', + title: 'TODO', + colour: '#ffd6a5', + zIndex: 4, + x: 12, + y: 34, + width: 210, + height: 140, + }); + + const viaJson = new Blockly.Workspace(); + const viaXml = new Blockly.Workspace(); + const jsonPlugin = new WorkspaceNotes(viaJson, {contextMenu: false}); + const xmlPlugin = new WorkspaceNotes(viaXml, {contextMenu: false}); + jsonPlugin.init(); + xmlPlugin.init(); + try { + Blockly.serialization.workspaces.load( + Blockly.serialization.workspaces.save(this.workspace), + viaJson, + ); + Blockly.Xml.domToWorkspace( + Blockly.Xml.workspaceToDom(this.workspace), + viaXml, + ); + + const describe = (ws) => + ws.getTopComments(false).map((n) => ({ + title: n.getTitle(), + colour: n.getColour(), + zIndex: n.getZIndex(), + text: n.getText(), + pinned: n.isPinned(), + xy: n.getRelativeToSurfaceXY(), + size: n.getSize(), + })); + + assert.deepEqual(describe(viaXml), describe(viaJson)); + } finally { + jsonPlugin.dispose(); + xmlPlugin.dispose(); + viaJson.dispose(); + viaXml.dispose(); + } + }); + + test('a note stays readable by plain Blockly', function () { + makeNote(this.workspace, { + text: 'body', + title: 'TODO', + colour: '#ffd6a5', + x: 40, + y: 20, + width: 250, + height: 130, + }); + const xml = Blockly.Xml.workspaceToDom(this.workspace); + + // Drop the plugin, so Blockly's own loader handles the same document. + this.plugin.dispose(); + const target = new Blockly.Workspace(); + try { + Blockly.Xml.domToWorkspace(xml, target); + const [comment] = target.getTopComments(false); + assert.isFalse(isNote(comment), 'plain Blockly makes a plain comment'); + assert.equal(comment.getText(), 'body'); + assert.equal(comment.getSize().width, 250); + } finally { + target.dispose(); + this.plugin.init(); + } + }); + }); + + suite('helpers', function () { + test('domToNoteState reads every attribute', function () { + const dom = Blockly.utils.xml.textToDom( + 'body', + ); + const [elem] = comments(dom); + assert.deepEqual(domToNoteState(elem), { + id: 'n1', + x: 1, + y: 2, + width: 3, + height: 4, + text: 'body', + collapsed: true, + editable: false, + movable: false, + deletable: false, + title: 'T', + colour: '#ffd6a5', + pinned: true, + locked: true, + zIndex: 7, + meta: {author: 'ada', createdAt: 'c', updatedAt: 'u'}, + }); + }); + + test('malformed geometry is skipped, not stored as NaN', function () { + const dom = Blockly.utils.xml.textToDom( + '', + ); + const state = domToNoteState(comments(dom)[0]); + assert.notProperty(state, 'x'); + assert.notProperty(state, 'y'); + assert.notProperty(state, 'width'); + assert.notProperty(state, 'height'); + }); + + test('noteToDom and domToNoteState are inverses', function () { + const note = makeNote(this.workspace, { + text: 'body', + title: 'T', + colour: '#ffd6a5', + zIndex: 5, + x: 11, + y: 22, + }); + const state = domToNoteState(noteToDom(note)); + assert.equal(state.title, 'T'); + assert.equal(state.colour, '#ffd6a5'); + assert.equal(state.zIndex, 5); + assert.equal(state.x, 11); + assert.equal(state.y, 22); + assert.equal(state.text, 'body'); + }); + }); + + suite('teardown', function () { + test('dispose restores Blockly’s own XML functions', function () { + const patched = Blockly.Xml.workspaceToDom; + this.plugin.dispose(); + try { + assert.notEqual(Blockly.Xml.workspaceToDom, patched); + makeNote(this.workspace, {title: 'ignored'}); + const [elem] = comments(Blockly.Xml.workspaceToDom(this.workspace)); + assert.isNull(elem.getAttribute('title')); + } finally { + this.plugin.init(); + } + }); + }); +}); diff --git a/blockly-workspace-notes/tsconfig.json b/blockly-workspace-notes/tsconfig.json new file mode 100644 index 0000000..ad4cfd0 --- /dev/null +++ b/blockly-workspace-notes/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "module": "es2015", + "moduleResolution": "bundler", + "target": "es6", + "strict": true, + + // Explicit rather than inherited. At `target: es6` the implied lib is + // ES2015, which has no `Object.values`; this compiled only because + // `@types/node` happens to reference `lib es2020`. `types: []` stops that + // accident — src/ is browser-only, and the Node-flavoured code lives in + // test/*.js, which is not part of this program. + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": [], + + "skipLibCheck": true, + + // Already the default at `target: es6`, set explicitly because it is + // load-bearing: a field definition emitted after `super()` would wipe + // state the base constructor had already caused to be built. + "useDefineForClassFields": false, + + "isolatedModules": true + }, + // test/**/* is JavaScript on purpose — @blockly/dev-scripts finds test + // entries with a literal `.mocha.js` filter — so it is never in the program. + "include": ["src"] +}