wysiwygido 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +61 -4
  3. package/dist/adapter.js +2 -3
  4. package/dist/{chart-CnJDVHSA.js → chart-D61cP2Zp.js} +13 -6
  5. package/dist/chart-D61cP2Zp.js.map +1 -0
  6. package/dist/collab.js +7 -7
  7. package/dist/collab.js.map +1 -1
  8. package/dist/colors-B3cH2hYo.js +107 -0
  9. package/dist/colors-B3cH2hYo.js.map +1 -0
  10. package/dist/{data-table-C972cOYM.js → data-table-CgmN4xc4.js} +4 -6
  11. package/dist/data-table-CgmN4xc4.js.map +1 -0
  12. package/dist/{data-table-app-CI6OXjcZ.js → data-table-app-B2_xsug1.js} +10 -312
  13. package/dist/data-table-app-B2_xsug1.js.map +1 -0
  14. package/dist/data-table.js +3 -2
  15. package/dist/{dialog-DVjlHRqv.js → dialog-CBVhuASJ.js} +3 -5
  16. package/dist/{dialog-DVjlHRqv.js.map → dialog-CBVhuASJ.js.map} +1 -1
  17. package/dist/docx.d.ts +5 -0
  18. package/dist/docx.js +117 -66
  19. package/dist/docx.js.map +1 -1
  20. package/dist/editor.d.ts +9 -0
  21. package/dist/epub.js +2 -3
  22. package/dist/epub.js.map +1 -1
  23. package/dist/floating-bar.d.ts +48 -0
  24. package/dist/formula-bar.d.ts +31 -0
  25. package/dist/formula-catalog.d.ts +8 -0
  26. package/dist/formula-core.d.ts +136 -0
  27. package/dist/formula-eval.d.ts +13 -0
  28. package/dist/formula-lib-data.d.ts +5 -0
  29. package/dist/formula-lib-math.d.ts +9 -0
  30. package/dist/formula-lib-text.d.ts +7 -0
  31. package/dist/formula-lib.d.ts +3 -0
  32. package/dist/grid-formulas-CFGWbz2E.js +4076 -0
  33. package/dist/grid-formulas-CFGWbz2E.js.map +1 -0
  34. package/dist/grid-formulas.d.ts +85 -0
  35. package/dist/icons-CLvSP8nf.js +166 -0
  36. package/dist/icons-CLvSP8nf.js.map +1 -0
  37. package/dist/index.d.ts +4 -0
  38. package/dist/inert.d.ts +6 -0
  39. package/dist/office-chrome-BmnDYfog.js +784 -0
  40. package/dist/office-chrome-BmnDYfog.js.map +1 -0
  41. package/dist/office-chrome.d.ts +147 -0
  42. package/dist/paste.d.ts +6 -6
  43. package/dist/pdf.js +1 -2
  44. package/dist/picture-styles.d.ts +17 -0
  45. package/dist/plugins/context-menu.d.ts +6 -0
  46. package/dist/plugins/find-replace.d.ts +9 -2
  47. package/dist/plugins/index.d.ts +2 -1
  48. package/dist/plugins/offline.d.ts +10 -0
  49. package/dist/plugins/tab-key.d.ts +11 -0
  50. package/dist/plugins/table-formulas.d.ts +4 -6
  51. package/dist/presentation-app.d.ts +72 -0
  52. package/dist/presentation-editor.d.ts +103 -0
  53. package/dist/presentation-model.d.ts +185 -0
  54. package/dist/presentation-render.d.ts +20 -0
  55. package/dist/presentation-show.d.ts +16 -0
  56. package/dist/presentation.js +4272 -0
  57. package/dist/presentation.js.map +1 -0
  58. package/dist/react.js +2 -3
  59. package/dist/resize.d.ts +5 -0
  60. package/dist/ribbon.d.ts +19 -0
  61. package/dist/spreadsheet-app.d.ts +88 -0
  62. package/dist/spreadsheet-model.d.ts +260 -0
  63. package/dist/spreadsheet-view.d.ts +138 -0
  64. package/dist/spreadsheet.js +5386 -0
  65. package/dist/spreadsheet.js.map +1 -0
  66. package/dist/{src-BNH7F1TH.js → src-D4NV1D9Y.js} +4192 -3900
  67. package/dist/src-D4NV1D9Y.js.map +1 -0
  68. package/dist/style.css +350 -2
  69. package/dist/svelte.js +1 -2
  70. package/dist/themes-B7BDDWRQ.js +301 -0
  71. package/dist/themes-B7BDDWRQ.js.map +1 -0
  72. package/dist/vue.js +2 -3
  73. package/dist/wysiwygido.js +10 -6
  74. package/package.json +15 -7
  75. package/dist/chart-CnJDVHSA.js.map +0 -1
  76. package/dist/data-table-C972cOYM.js.map +0 -1
  77. package/dist/data-table-app-CI6OXjcZ.js.map +0 -1
  78. package/dist/src-BNH7F1TH.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/). Before 1.0.0 minor versions could contain breaking changes. From 1.0.0 on, only a major version may break the public API (see "Stability and versioning" in the README); changes are listed under "Changed" or "Removed".
4
4
 
5
+ ## 1.3.0 - 2026-10-25
6
+
7
+ - **Security audit, second part**: pasted HTML can no longer carry tracked-change authors, mention ids or comment ids; an embed from the page's own origin never gets `allow-same-origin`; `encryptedDraftStore`, `deriveDraftKey` and `IndexedDBDraftStore.clear()` for drafts on shared computers; a regex replacement keeps lookbehind and anchors (`$1` is expanded from the match as found); adding a comment finds its words again across bold or italic runs; rejected pictures and unreadable files are reported (`image-rejected`, `error`, `import` events, no more silent failures); pictures over 25 MB are refused; optional peers `katex >=0.18.2`, `mermaid >=12.1.0`, `vue >=3.5.0`; `publint` and `attw` are pinned in `check:package`.
8
+ - **Security audit** (OWASP Top 10, ASVS, NIST SP 800-53 SI-10 / SSDF). Fixed: mutation XSS through the version comparison and other markup that keeps SVG or MathML (the cleaners now remove `style`, `noscript`, `template`, `mglyph`, `annotation-xml` and re-parse until the output is stable); the Source view could change a read-only document; a Word paste with thousands of conditional comments and a find-and-replace pattern such as `(a|a)+$` froze the tab; Markdown import and the image view accepted `//host`, `ftp:` and `smb:` addresses; `/\host` and `/<tab>/host` passed the link check; a header or footer could close the print `<style>` and run script (the print frame is sandboxed now too); `.docx` import had no limit on the unpacked size (zip bomb) and `.docx` export wrote control characters that make Word call the file corrupt, and `file:` and `smb:` links; the Markdown code fence could be closed by the code itself; file-name checks missed `evil.exe.`, double extensions, direction overrides and many script types; colours, fonts, table backgrounds, image widths and presence colours from collaborators are re-checked when drawn; an offline draft without comments no longer breaks a restore; a data-table column called `constructor` no longer counts as filtered. Spreadsheet and presentation: CSV export neutralises formula text, the formula engine has no regular-expression functions and a work budget, `A:A`-style wildcards cannot backtrack, a CSV of 150,000 rows imports, pictures are capped (file, pixels, deck), text dropped into a slide is plain text, print frames are sandboxed. Server: `CREATE_KEY`, `CREATE_LIMIT`, `MAX_DOCS`, `TRUST_PROXY` and `HOST` from the environment, a commenter can no longer delete or rewrite others' comments (and the role is re-checked inside the lock), a malformed URL is a 400, the WebSocket checks the origin when none is configured; the example API checks Host and Origin, keeps its version after a restart and writes through a unique temporary file. Dependencies: `markdown-it` 15.0.3 (a quadratic-time link-reference bug), `vite` 8.3.5, `dompurify` override; `pnpm audit` now reports one low advisory, in a development-only path.
9
+
10
+ - **Spreadsheet** (`wysiwygido/spreadsheet`, `createSpreadsheet`): an Excel-style spreadsheet with formulas, formats, sheets, sort, filter, fill, freeze panes, undo, CSV, a formula bar and the menus as a classic ribbon or a floating card. The formula engine is new: about 400 Excel functions, TRUE/FALSE, arrays, dynamic arrays that spill, `LET`, other-sheet references, whole columns, Excel date serials and `TEXT` formats. The formula editor above the grid (name box list, cancel and confirm, `fx` function list, a field that expands, suggestions and syntax hint) can be shown or hidden, and `menu` takes `'classic'`, `'floating'`, `'menubar'`, `'formula'` or `'none'`. Editor tables keep their own, older behaviour (comparisons give 1 and 0, `^` goes right to left) and now also understand `$A$1`, `COUNTIF`, `SUMIF`, `MEDIAN`, `COUNTA`, `AND`, `OR`, `NOT`, `LEN`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `MOD`, `INT`, `POWER` and `PRODUCT` too.
11
+ - **Presentation** (`wysiwygido/presentation`, `createPresentation`): a PowerPoint-style slide editor with layouts, themes, text boxes, shapes and pictures, transitions, a slide show with a presenter view, and export to JSON or a web page. Same two menu looks.
12
+
13
+ - Floating menu: the `floating` option puts the toolbar (or ribbon) in a rounded card with a soft shadow, a header (document name, save status from `Autosave`, your own controls) and a menu bar with File, Edit, View, Insert and Format drop-downs. Exports `FloatingBar`, `defaultFloatingMenus`.
14
+
15
+ - A numbered or bulleted heading now has the collapse arrow too (left of the number); it hides what follows inside its list item.
16
+
17
+ - Added: the **Tab key** (`TabKey`, in `defaultPlugins`). Tab used to move the focus out of the editor. Now it indents a paragraph or heading (Shift+Tab takes it back), moves to the next table cell (and adds a row after the last one), and keeps its list and code block behaviour. Escape, then Tab, still moves the focus on, so keyboard users are not trapped. Not active in a read-only editor; leave the plugin out to keep the browser's behaviour.
18
+
19
+ - Added: **picture styles**, like the gallery in Word: 28 looks for an image (frames, shadows, rounded and oval shapes, soft edges, diagonal corners, tilt and perspective, reflections, metal frames) in a gallery in the Picture tab and in the right-click menu, with Clear Style. Command `pictureStyle(id | null)`; `PICTURE_STYLES` lists them. Only the id is stored (`data-picture-style`), the CSS comes from a fixed table. Word and Markdown export write the picture without the effects; Firefox does not draw the reflections.
20
+ - Fixed: the number or bullet of a list item that starts with a heading was as small as body text; it now takes the heading's size and weight.
21
+ - Changed: a long ribbon menu (such as the gallery) scrolls inside the editor instead of being cut off by its edge.
22
+
23
+ ## 1.2.1
24
+
25
+ - Fixed: bullets and numbering did nothing on a heading (the "Bulleted list" and "Numbered list" buttons, and the commands). A list item had to start with a paragraph, so a heading could not be wrapped. An item may now start with a heading, which keeps its heading style and gets the bullet or number (numbered headings, as in Word); turning the list off gives the heading back, and switching between bullets and numbers works. Word export writes such an item with the heading style and the numbering.
26
+
5
27
  ## 1.2.0
6
28
 
7
29
  - Added: **collapsible headings** (`CollapsibleHeadings`, in `defaultPlugins`): an arrow in the margin of every top-level heading hides the text under it up to the next heading of the same or a higher level, like Word. Commands `toggleHeadingFold`, `collapseHeadings`, `expandHeadings`, `showHeadingLevel(n)`; right-click menu, ribbon (View → Headings) and shortcuts included. The document is not changed and the pages are laid out again without the hidden text.
package/README.md CHANGED
@@ -161,6 +161,12 @@ Markdown), so exports show it. A reference to a deleted caption says "Missing re
161
161
 
162
162
  **Columns**: `Columns()` flows blocks into 2–4 newspaper columns (`insertColumns(n)` wraps the selection, `setColumns({ count, rule, gap })`, `removeColumns`). `setDocumentColumns(count)` puts the whole page (below a leading heading) in columns and `insertColumnBreak` starts the next column. Columns collapse to one on narrow screens. Word export writes every top-level columns block as its own continuous **section** with the real column count, gap and rule (text before and after stays single-column; headers and footers stay on the first section); Markdown flows the text in one column. Columns nested inside columns are flattened in Word.
163
163
 
164
+ **Picture styles**: select an image and open Picture → Picture Styles for a gallery of 28 looks (frames, offset and soft shadows, rounded and oval shapes, soft edges, diagonal corners, tilt and perspective, reflections, metal frames), plus Clear Style; the right-click menu on an image has the same list. `pictureStyle(id)` applies one to the selected image and `pictureStyle(null)` clears it; `PICTURE_STYLES` from `wysiwygido` lists the ids and labels. The image stores only the id (`data-picture-style`); the CSS comes from a fixed table, so a document cannot inject styles, and it is written inline so the look also shows in exported HTML. Reflections use `-webkit-box-reflect`, which Firefox does not draw (the picture is shown without the reflection there), and the Word and Markdown exports write the picture without the effects.
165
+
166
+ **The Tab key**: `TabKey` (in `defaultPlugins`) makes Tab work as in Word instead of moving the focus out of the editor: in a paragraph or heading it indents (Shift+Tab takes it back, in steps of half an inch, over every paragraph in the selection), in a table it moves to the next cell and adds a row after the last one, in a list it changes the level, and in a code block it indents the code. So that keyboard users are never trapped, **Escape, then Tab** moves the focus on (any other key puts Tab back to work). Leave `TabKey` out of `plugins`, or filter it from `defaultPlugins`, to keep the browser's behaviour; it does nothing in a read-only editor.
167
+
168
+ **Floating menu**: `floating: true` puts the toolbar (or the ribbon) in a rounded card with a soft shadow that stays in view: a header with the document name, the save status (from `Autosave`) and your own controls, then a menu bar (File, Edit, View, Insert, Format) whose drop-downs are built from the commands the editor has, then the toolbar. Choose the content with `floating: { title, onTitle, menus, status, end }`: `onTitle` makes the name editable, `menus` replaces the default menus (`false` hides the bar), `end` is an element for the right of the header (avatars, a Share button). Combine with `toolbar: { overflow: 'more' }` for a single row.
169
+
164
170
  **Collapsible headings**: `CollapsibleHeadings` (in `defaultPlugins`) puts a small arrow in the margin left of every top-level heading, as in Word: a click hides everything up to the next heading of the same or a higher level and shows "…" after the title, and a click again opens it. Commands: `toggleHeadingFold` (the heading at the cursor, or the nearest one above it), `collapseHeadings`, `expandHeadings` and `showHeadingLevel(n)` (only the headings down to level `n` stay open); they are also in the right-click menu on a heading and in the ribbon under View → Headings. Shortcuts: Ctrl/Cmd + Alt + Up collapses, Ctrl/Cmd + Alt + Down expands everything. Nothing is removed from the document: the HTML, undo, export and print are unchanged, the pages are laid out again without the hidden text, and a cursor that moves into hidden text (search, undo, the outline) opens that section. Which headings are collapsed is view state: it is not saved with the document.
165
171
 
166
172
  **Charts**: `Charts()` draws bar, line and pie charts as SVG. With the cursor in a table, `insertChart('bar' | 'line' | 'pie')` uses the first row as series and the first column as labels (numbers like `1,200`, `$40` or `12%` are understood); `insertChart(type, { labels, series })` takes data directly and `setChart` changes the selected chart. The chart is saved as validated data (never markup) and redrawn on load; Word export writes it as a picture (needs a browser canvas) and Markdown as a table.
@@ -596,6 +602,50 @@ Rows can be arrays in column order or objects by column key (objects are easier
596
602
 
597
603
  Events `table-ready` and `table-change` (`detail.table`); the element's `table` property sets or reads the table.
598
604
 
605
+ ## Spreadsheet
606
+
607
+ A spreadsheet in the manner of Excel on the web, with no editor behind it (`wysiwygido/spreadsheet`, a separate entry point that pulls in nothing else; the build checks that). It has **formulas the way Excel writes them**: about 400 functions (math and trig, statistics and distributions, text, date and time, logic, lookup with `VLOOKUP`, `XLOOKUP`, `INDEX`/`MATCH`, finance, engineering, database, information), TRUE and FALSE, arrays (`SUMPRODUCT((A1:A9>5)*B1:B9)`, `IF` over a range), dynamic arrays that spill (`SORT`, `FILTER`, `UNIQUE`, `SEQUENCE`, `TRANSPOSE`, `VSTACK`…), `LET`, references to other sheets (`=Budget!B2*2`, `='My sheet'!A1:A9`), whole columns (`A:A`), `INDIRECT` and `OFFSET`, `TEXT` with number and date formats, Excel's date serial numbers, and the same errors (`#DIV/0!`, `#N/A`, `#VALUE!`, `#NAME?`…), never `eval`. The **formula editor** above the grid has a name box with a list, cancel and confirm buttons, an `fx` button with the functions by category, a field that opens to several lines (Alt+Enter adds a line), function names as you type (Tab takes one) and the syntax of the function you are in. Number formats (general, number, currency, percent, date, text), fonts, colours, borders and alignment, several sheets, fill by dragging the corner (numbers, days, months, "Item 1"), sort, filter by value, freeze panes, insert and delete of rows and columns (formulas follow), resize and autofit, find and replace, a name box and formula bar, copy and paste with Excel and Google Sheets (what you paste in is text; what you copy out also carries the formats as an HTML table), CSV in and out, undo and redo, zoom, print, and a status bar that sums the selection. Only the visible cells are drawn, so a sheet of 20,000 rows scrolls as smoothly as one of 20.
608
+
609
+ ```ts
610
+ import { createSpreadsheet } from 'wysiwygido/spreadsheet';
611
+ import 'wysiwygido/style.css';
612
+
613
+ const sheet = createSpreadsheet({
614
+ element,
615
+ menu: 'classic', // 'classic' ribbon · 'floating' card · 'menubar' (only the drop-down menus) · 'formula' (only the formula editor) · 'none'
616
+ data: { sheets: [{ name: 'Budget', cells: { A1: { v: 'Rent' }, B1: { v: '1200', s: { fmt: 'currency' } }, B2: { v: '=B1*12' } } }] },
617
+ title: 'Yearly expenses', onTitle: (name) => rename(name), // the floating card shows and edits the name
618
+ save: (data) => fetch('/api/book', { method: 'PUT', body: JSON.stringify(data) }), // header: Saving… / Saved
619
+ // storageKey: 'my-book', // or keep it in localStorage
620
+ });
621
+ sheet.setValue('C1', '=SUM(B1:B2)');
622
+ sheet.getValue('C1'); // 14400 + 1200
623
+ sheet.toCSV(); sheet.getData(); sheet.setMenu('floating');
624
+ ```
625
+
626
+ The two **menus** share the same commands. The classic ribbon has tabs with labelled groups; the floating menu is a card with a header (document name, save status, your own `end` element for avatars or a Share button), a menu bar with drop-downs, and a one-row toolbar whose last items fold into "⋯" when the window is narrow. `menu` also takes `'menubar'` (the card with only its drop-down menus), `'formula'` (only the formula editor) and `'none'`; `setMenu(mode)` changes it while running and `setFormulaBar(false)` hides the formula editor. Options: `theme` (`'light'`, `'dark'`, `'auto'` or any colour theme of the editor), `readOnly`, `height`, `formulaBar`, `sheetTabs`, `statusBar`, `gridlines`, `headings`, `currency`, `locale`, `onChange`. Not included: merged cells, charts, conditional formats, defined names in the UI, `LAMBDA` and its helpers, complex-number, bond, cube and web functions (those give `#NAME?`).
627
+
628
+ ## Presentation
629
+
630
+ A slide editor in the manner of PowerPoint and Google Slides, also without the document editor (`wysiwygido/presentation`). Slides have text boxes (real rich text: bold, italic, underline, colours, sizes, links, bullets), shapes (12 kinds, fill and outline) and pictures (drop, paste or pick a file; large ones are shrunk). Boxes can be moved with snapping guides, resized, rotated, aligned, distributed, stacked and locked. There are six layouts, six themes, 16:9 and 4:3, a background per slide, speaker notes, transitions (fade, push, zoom), a slide strip with drag to reorder, a slide sorter, undo and redo, gridlines and zoom. **Slide show** (F5, or Shift+F5 from the current slide) runs full screen with the keyboard, a click or a swipe, a black screen (B) and a **presenter view** (P: the slide, the next slide, the notes and a clock). Export gives the deck as JSON, as one self-contained web page (arrow keys to move, one slide per printed page) or as a PDF through print.
631
+
632
+ ```ts
633
+ import { createPresentation } from 'wysiwygido/presentation';
634
+ import 'wysiwygido/style.css';
635
+
636
+ const deck = createPresentation({
637
+ element,
638
+ menu: 'floating', // or 'classic': File, Home, Insert, Design, Transitions, Slide Show, View
639
+ data: { title: 'Q4 review', theme: 'ocean', slides: [{ layout: 'title', els: [], notes: '' }] },
640
+ save: (data) => fetch('/api/deck', { method: 'PUT', body: JSON.stringify(data) }),
641
+ });
642
+ deck.addSlide('titleContent');
643
+ deck.present(0); // deck.present(0, true) opens the presenter view
644
+ const page = deck.toHTML(); // everything in one file
645
+ ```
646
+
647
+ Everything that comes from outside (a loaded file, pasted text, a dropped picture) is cleaned: text keeps a short list of tags and styles, links keep `http(s)`, `mailto` and `tel`, pictures must be `data:` images or `http(s)`, so a deck from someone else cannot run code. `PowerPoint (.pptx)` files are not read or written.
648
+
599
649
  ## Themes
600
650
 
601
651
  Ten colour themes ship with the editor: `light`, `dark`, `sepia`, `ocean`, `forest`, `rose`, `solarized`, `midnight`, `nord` and `high-contrast`. A theme restyles the toolbar, ribbon, panels and the paper, and only inside the editor.
@@ -674,10 +724,17 @@ Leave the plugin out of `plugins` to have no custom menu.
674
724
 
675
725
  ## Security notes
676
726
 
677
- Pasted/loaded HTML goes through the schema: unknown tags and attributes are dropped; link and image URLs are limited to
678
- safe schemes (`javascript:` is rejected); Markdown import disables raw HTML; user text (comments, mentions, footnotes, AI
679
- output) is rendered with `textContent`; the server compares secrets in constant time, stores only hashes, bounds request
680
- size and treats unknown documents and bad credentials identically.
727
+ **What the library does.** Pasted and loaded HTML goes through the schema: unknown tags and attributes are dropped; link and image URLs are limited to safe schemes (`javascript:`, `//host`, `/\host` are rejected), also when they come from Markdown, from a collaborator or from a saved draft; Markdown import disables raw HTML; user text (comments, mentions, footnotes, AI output) is rendered with `textContent`. Markup that has to keep SVG or MathML (a formula, a diagram, a version comparison) is cleaned and then parsed again until it no longer changes, which stops mutation XSS. Word and PDF imports have size and page limits, the `.docx` importer reads the sizes the archive declares and refuses a zip bomb, and the `.docx` writer drops characters XML does not allow and writes only web, mail and phone links. File names lose control characters, direction overrides and trailing dots, and double extensions are checked. CSV exports prefix text that starts with `=`, `+`, `-` or `@` so a spreadsheet does not run it. The spreadsheet formula engine has no `eval`, no regular expressions from the sheet and a work budget per cell and per sheet (`#CALC!` beyond it). The server compares secrets in constant time, stores only hashes, bounds request size, lets a commenter add comments but not delete other people's, and treats unknown documents and bad credentials identically.
728
+
729
+ **What stays with you.**
730
+ - *Content Security Policy.* The library needs no `eval` or `new Function`. It does need `style-src 'unsafe-inline'` (the editor positions things with inline styles), `img-src` for the picture sources you allow (`data:` and `blob:` if you use them) and, for embeds, `frame-src` for the providers you enable.
731
+ - *Server side.* The editor cleans HTML in the browser; a client can still send anything to your server. Sanitise HTML with an HTML sanitiser before you store or serve it, and check permissions on the server.
732
+ - *Restricted editing and read-only* guard the interface, not the data: a determined user can change what the browser sends. Enforce them on the server.
733
+ - *Review marks.* A paste cannot carry the author of a tracked change, a mention id or a comment id (they are removed). Marks that come from your own storage or from collaborators are trusted as stored: assign authors on the server.
734
+ - *Privacy.* The spell checker sends paragraphs to the LanguageTool address you configure (the default is the public service); the AI plugins send text to your provider; remote pictures are fetched by the reader's browser; drafts, versions and the spreadsheet and presentation `storageKey` data stay in the browser's storage until you clear it. For a shared computer use `encryptedDraftStore(store, await deriveDraftKey(passphrase, salt))` for `Offline`, and `IndexedDBDraftStore.clear()` on sign-out. Rejected pictures are reported as the `image-rejected` event (editor), `error` (presentation) and `import` / `error` (spreadsheet).
735
+ - *Server settings.* `pnpm server` listens on `127.0.0.1`. Set `CREATE_KEY`, `CREATE_LIMIT` and `MAX_DOCS` before you expose it, `TRUST_PROXY=1` when it sits behind a reverse proxy, and `ALLOWED_ORIGIN` for cross-origin use.
736
+
737
+ Report a vulnerability privately to the maintainer rather than in a public issue.
681
738
 
682
739
  ## License
683
740
 
package/dist/adapter.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as createEditor } from "./src-BNH7F1TH.js";
2
- //#region src/adapter.ts
1
+ import { t as createEditor } from "./src-D4NV1D9Y.js";
2
+ // src/adapter.ts
3
3
  /**
4
4
  * Framework-neutral binding for anything that needs a "value in, value out" control: Angular's `ControlValueAccessor`, Svelte, Solid,
5
5
  * Lit, plain scripts. It owns the editor's life (create, keep the value and read-only state in sync without echo loops, destroy).
@@ -52,7 +52,6 @@ function bindEditor(element, config = {}) {
52
52
  }
53
53
  };
54
54
  }
55
- //#endregion
56
55
  export { bindEditor };
57
56
 
58
57
  //# sourceMappingURL=adapter.js.map
@@ -1,4 +1,13 @@
1
- //#region src/crop.ts
1
+ // src/url.ts
2
+ /** Where a link may point: http(s), mailto, tel, an in-page anchor or a same-site path. `//host` (protocol-relative) is not allowed. */
3
+ var HREF = /^(?:https?:|mailto:|tel:|#|\/(?![\/\\\t\r\n]))/i;
4
+ /** Where an image may come from: http(s), a same-site path, or a base64 raster data URL (not SVG). */
5
+ var SRC = /^(?:https?:|data:image\/(?:png|jpe?g|gif|webp|bmp|avif);base64,|\/(?![\/\\\t\r\n]))/i;
6
+ var isSafeHref = (u) => typeof u === "string" && HREF.test(u);
7
+ var isSafeSrc = (u) => typeof u === "string" && SRC.test(u);
8
+ /** Make a URL safe to place inside Markdown `(...)`: characters that end or split the destination are percent-encoded. */
9
+ var markdownUrl = (u) => u.replace(/[()\s<>\\]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
10
+ // src/crop.ts
2
11
  /** The smallest visible share of the image on either axis (so a crop can never collapse to nothing). */
3
12
  var MIN_VISIBLE = .05;
4
13
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
@@ -126,8 +135,7 @@ async function cropBytes(data, crop) {
126
135
  return null;
127
136
  }
128
137
  }
129
- //#endregion
130
- //#region src/chart.ts
138
+ // src/chart.ts
131
139
  var TYPES = [
132
140
  "bar",
133
141
  "line",
@@ -274,7 +282,6 @@ function chartSVG(spec, width = 560, height = 320) {
274
282
  if (legendH) parts.push(legend(spec.series.map((s) => s.name)));
275
283
  return `${head}${parts.join("")}</svg>`;
276
284
  }
277
- //#endregion
278
- export { MIN_VISIBLE as a, cropLayout as c, normalizeCrop as d, visibleSize as f, specFromRows as i, cropToString as l, chartSVG as n, cropBytes as o, cleanChart as r, cropFromString as s, CHART_COLORS as t, dragCrop as u };
285
+ export { MIN_VISIBLE as a, cropLayout as c, normalizeCrop as d, visibleSize as f, markdownUrl as h, specFromRows as i, cropToString as l, isSafeSrc as m, chartSVG as n, cropBytes as o, isSafeHref as p, cleanChart as r, cropFromString as s, CHART_COLORS as t, dragCrop as u };
279
286
 
280
- //# sourceMappingURL=chart-CnJDVHSA.js.map
287
+ //# sourceMappingURL=chart-D61cP2Zp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chart-D61cP2Zp.js","names":[],"sources":["../src/url.ts","../src/crop.ts","../src/chart.ts"],"sourcesContent":["/** Where a link may point: http(s), mailto, tel, an in-page anchor or a same-site path. `//host` (protocol-relative) is not allowed. */\nconst HREF = /^(?:https?:|mailto:|tel:|#|\\/(?![\\/\\\\\\t\\r\\n]))/i;\n/** Where an image may come from: http(s), a same-site path, or a base64 raster data URL (not SVG). */\nconst SRC = /^(?:https?:|data:image\\/(?:png|jpe?g|gif|webp|bmp|avif);base64,|\\/(?![\\/\\\\\\t\\r\\n]))/i;\n\nexport const isSafeHref = (u: unknown): u is string => typeof u === 'string' && HREF.test(u);\nexport const isSafeSrc = (u: unknown): u is string => typeof u === 'string' && SRC.test(u);\n\n/** Make a URL safe to place inside Markdown `(...)`: characters that end or split the destination are percent-encoded. */\nexport const markdownUrl = (u: string): string => u.replace(/[()\\s<>\\\\]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`);\n","/**\n * Image cropping math. A crop is the fraction of the *natural* image removed from each side, so it survives\n * resizing and is independent of how large the image is shown.\n */\nexport interface Crop { left: number; top: number; right: number; bottom: number }\nexport interface Natural { w: number; h: number }\n\n/** The smallest visible share of the image on either axis (so a crop can never collapse to nothing). */\nexport const MIN_VISIBLE = 0.05;\n\nconst clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));\nconst round = (v: number) => Math.round(v * 10000) / 10000;\n\n/** Clamp a crop into a valid range; null means \"no crop\". Junk values are treated as 0. */\nexport function normalizeCrop(input: Partial<Crop> | null | undefined): Crop | null {\n if (!input || typeof input !== 'object') return null;\n const n = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? clamp(v, 0, 1 - MIN_VISIBLE) : 0);\n let { left, top, right, bottom } = { left: n(input.left), top: n(input.top), right: n(input.right), bottom: n(input.bottom) };\n // Keep at least MIN_VISIBLE of each axis: shrink the larger side first.\n const fit = (a: number, b: number): [number, number] => {\n const excess = a + b - (1 - MIN_VISIBLE);\n if (excess <= 0) return [a, b];\n return a >= b ? [a - excess, b] : [a, b - excess];\n };\n [left, right] = fit(left, right);\n [top, bottom] = fit(top, bottom);\n const c = { left: round(left), top: round(top), right: round(right), bottom: round(bottom) };\n return c.left || c.top || c.right || c.bottom ? c : null;\n}\n\n/** Natural size of the visible region (what a crop leaves), in natural px. */\nexport function visibleSize(crop: Crop | null, nat: Natural): Natural {\n const c = crop ?? { left: 0, top: 0, right: 0, bottom: 0 };\n return { w: nat.w * (1 - c.left - c.right), h: nat.h * (1 - c.top - c.bottom) };\n}\n\nexport interface CropLayout {\n /** Size of the visible box. */\n width: number;\n height: number;\n /** Size and offset of the full image inside the box. */\n imgWidth: number;\n imgHeight: number;\n offsetX: number;\n offsetY: number;\n}\n\n/** Lay out a crop so the visible box is `displayWidth` px wide. */\nexport function cropLayout(crop: Crop, nat: Natural, displayWidth: number): CropLayout {\n const vw = 1 - crop.left - crop.right;\n const vh = 1 - crop.top - crop.bottom;\n const width = displayWidth;\n const height = (width * (vh * nat.h)) / (vw * nat.w);\n const imgWidth = width / vw;\n const imgHeight = height / vh;\n return { width, height, imgWidth, imgHeight, offsetX: -crop.left * imgWidth, offsetY: -crop.top * imgHeight };\n}\n\n/** Serialize as \"left,top,right,bottom\" for a data attribute. */\nexport const cropToString = (c: Crop) => `${c.left},${c.top},${c.right},${c.bottom}`;\n\nexport function cropFromString(s: string | null): Crop | null {\n if (!s) return null;\n const p = s.split(',').map(Number);\n // Data from HTML is untrusted: out-of-range values mean the markup is bogus, so reject it instead of clamping.\n if (p.length !== 4 || p.some((x) => !Number.isFinite(x) || x < 0 || x >= 1)) return null;\n return normalizeCrop({ left: p[0], top: p[1], right: p[2], bottom: p[3] });\n}\n\nexport type Handle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' | 'move';\n\n/**\n * Apply a drag of (dx, dy) fractions of the full image to a crop rectangle. Edges move independently,\n * `move` slides the whole rectangle without resizing it. Result is always valid.\n */\nexport function dragCrop(start: Crop, handle: Handle, dx: number, dy: number): Crop {\n let { left, top, right, bottom } = start;\n if (handle === 'move') {\n const w = 1 - left - right;\n const h = 1 - top - bottom;\n left = clamp(left + dx, 0, 1 - w);\n top = clamp(top + dy, 0, 1 - h);\n right = 1 - w - left;\n bottom = 1 - h - top;\n } else {\n if (handle.includes('w')) left = clamp(left + dx, 0, 1 - right - MIN_VISIBLE);\n if (handle.includes('e')) right = clamp(right - dx, 0, 1 - left - MIN_VISIBLE);\n if (handle.includes('n')) top = clamp(top + dy, 0, 1 - bottom - MIN_VISIBLE);\n if (handle.includes('s')) bottom = clamp(bottom - dy, 0, 1 - top - MIN_VISIBLE);\n }\n return { left: round(left), top: round(top), right: round(right), bottom: round(bottom) };\n}\n\n/**\n * Render the cropped region of an image to PNG bytes (browser only: needs canvas). Returns null when canvas\n * is unavailable, so callers can fall back to the full image.\n */\nexport async function cropBytes(data: Uint8Array, crop: Crop): Promise<{ data: Uint8Array; width: number; height: number } | null> {\n try {\n if (typeof createImageBitmap !== 'function') return null;\n const bmp = await createImageBitmap(new Blob([data as Uint8Array<ArrayBuffer>]));\n const sx = Math.round(bmp.width * crop.left);\n const sy = Math.round(bmp.height * crop.top);\n const sw = Math.max(1, Math.round(bmp.width * (1 - crop.left - crop.right)));\n const sh = Math.max(1, Math.round(bmp.height * (1 - crop.top - crop.bottom)));\n const canvas = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(sw, sh) : Object.assign(document.createElement('canvas'), { width: sw, height: sh });\n const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | null;\n if (!ctx) return null;\n ctx.drawImage(bmp, sx, sy, sw, sh, 0, 0, sw, sh);\n const blob: Blob | null = 'convertToBlob' in canvas ? await canvas.convertToBlob({ type: 'image/png' }) : await new Promise((r) => (canvas as HTMLCanvasElement).toBlob(r, 'image/png'));\n return blob ? { data: new Uint8Array(await blob.arrayBuffer()), width: sw, height: sh } : null;\n } catch {\n return null;\n }\n}\n","export type ChartType = 'bar' | 'line' | 'pie';\nexport interface ChartSpec { type: ChartType; title: string; labels: string[]; series: { name: string; values: number[] }[] }\n\nconst TYPES: ChartType[] = ['bar', 'line', 'pie'];\nexport const CHART_COLORS = ['#2563eb', '#ea580c', '#16a34a', '#9333ea', '#dc2626', '#0891b2', '#ca8a04', '#db2777'];\nconst esc = (s: string) => s.replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' })[c]!);\nconst str = (v: unknown, max: number) => (typeof v === 'string' ? v.replace(/[\\u0000-\\u001f]/g, ' ').slice(0, max) : '');\n\n/** A well-formed spec from untrusted input (JSON text or an object), or null when there is nothing to draw. */\nexport function cleanChart(input: unknown): ChartSpec | null {\n let o: any = input;\n if (typeof input === 'string') { try { o = JSON.parse(input); } catch { return null; } }\n if (!o || typeof o !== 'object') return null;\n const labels = (Array.isArray(o.labels) ? o.labels : []).slice(0, 50).map((l: unknown) => str(l, 40));\n const series = (Array.isArray(o.series) ? o.series : []).slice(0, 8).map((s: any) => ({\n name: str(s?.name, 40),\n values: labels.map((_: string, i: number) => { const n = Number(s?.values?.[i]); return Number.isFinite(n) ? Math.max(-1e12, Math.min(1e12, n)) : 0; }),\n }));\n if (!labels.length || !series.length) return null;\n return { type: TYPES.includes(o.type) ? o.type : 'bar', title: str(o.title, 120), labels, series };\n}\n\n/** Parse \"1,234.5\", \"12%\", \"$40\" … ; NaN when it is not a number. */\nexport function parseNumber(text: string): number {\n const t = text.trim().replace(/[\\s$€£¥%]/g, '').replace(/,(?=\\d{3}(\\D|$))/g, '');\n return t && /^-?\\d*\\.?\\d+(e[+-]?\\d+)?$/i.test(t) ? Number(t) : NaN;\n}\n\n/** Chart data from a table: first row = series names, first column = labels, the rest numbers (a first row of numbers means no header). */\nexport function specFromRows(rows: string[][], type: ChartType = 'bar'): ChartSpec | null {\n if (rows.length < 2 || rows[0].length < 2) return null;\n const header = rows[0];\n const body = rows.slice(1);\n const cols = header.length - 1;\n const series = Array.from({ length: cols }, (_, c) => ({ name: header[c + 1] || `Series ${c + 1}`, values: body.map((r) => { const n = parseNumber(r[c + 1] ?? ''); return Number.isNaN(n) ? 0 : n; }) }));\n if (!series.some((s) => s.values.some((v) => v !== 0))) return null;\n return cleanChart({ type, title: '', labels: body.map((r) => r[0] ?? ''), series });\n}\n\nconst nice = (max: number) => { if (max <= 0) return 1; const p = 10 ** Math.floor(Math.log10(max)); const f = max / p; return (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10) * p; };\nconst fmt = (n: number) => (Math.abs(n) >= 1e6 ? `${+(n / 1e6).toFixed(1)}M` : Math.abs(n) >= 1e4 ? `${+(n / 1e3).toFixed(1)}k` : String(+n.toFixed(2)));\n\n/** Self-contained SVG (text in `currentColor`, fixed series colours) with an accessible title and description. */\nexport function chartSVG(spec: ChartSpec, width = 560, height = 320): string {\n const W = width, H = height;\n const head = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${W} ${H}\" width=\"${W}\" height=\"${H}\" role=\"img\" aria-label=\"${esc(spec.title || `${spec.type} chart`)}\" font-family=\"system-ui, sans-serif\" font-size=\"12\">`;\n const parts: string[] = [`<title>${esc(spec.title || `${spec.type} chart`)}</title>`];\n const top = spec.title ? 34 : 14;\n if (spec.title) parts.push(`<text x=\"${W / 2}\" y=\"22\" text-anchor=\"middle\" font-size=\"15\" font-weight=\"700\" fill=\"currentColor\">${esc(spec.title)}</text>`);\n const legendH = spec.type === 'pie' || spec.series.length > 1 ? 24 : 0;\n const color = (i: number) => CHART_COLORS[i % CHART_COLORS.length];\n const legend = (items: string[]) => {\n let x = 12;\n return items.map((n, i) => { const g = `<rect x=\"${x}\" y=\"${H - 18}\" width=\"10\" height=\"10\" rx=\"2\" fill=\"${color(i)}\"/><text x=\"${x + 14}\" y=\"${H - 9}\" fill=\"currentColor\">${esc(n)}</text>`; x += 26 + n.length * 7; return g; }).join('');\n };\n if (spec.type === 'pie') {\n const vals = spec.series[0].values.map((v) => Math.max(0, v));\n const total = vals.reduce((a, b) => a + b, 0) || 1;\n const cx = W / 2, cy = top + (H - top - legendH - 8) / 2, r = Math.min(W / 2 - 20, (H - top - legendH - 16) / 2);\n let a0 = -Math.PI / 2;\n vals.forEach((v, i) => {\n if (v <= 0) return;\n const a1 = a0 + (v / total) * Math.PI * 2;\n const big = a1 - a0 > Math.PI ? 1 : 0;\n const p = (a: number) => `${(cx + r * Math.cos(a)).toFixed(2)} ${(cy + r * Math.sin(a)).toFixed(2)}`;\n parts.push(v / total >= 0.9999 ? `<circle cx=\"${cx}\" cy=\"${cy}\" r=\"${r}\" fill=\"${color(i)}\"/>` : `<path d=\"M${cx} ${cy} L${p(a0)} A${r} ${r} 0 ${big} 1 ${p(a1)} Z\" fill=\"${color(i)}\" stroke=\"#fff\" stroke-width=\"1.5\"/>`);\n if (v / total > 0.06) { const am = (a0 + a1) / 2; parts.push(`<text x=\"${(cx + r * 0.62 * Math.cos(am)).toFixed(1)}\" y=\"${(cy + r * 0.62 * Math.sin(am) + 4).toFixed(1)}\" text-anchor=\"middle\" fill=\"#fff\" font-weight=\"700\">${Math.round((v / total) * 100)}%</text>`); }\n a0 = a1;\n });\n parts.push(legend(spec.labels));\n return `${head}${parts.join('')}</svg>`;\n }\n const left = 46, right = 14, bottom = 28 + legendH;\n const pw = W - left - right, ph = H - top - bottom;\n const all = spec.series.flatMap((s) => s.values);\n const max = nice(Math.max(0, ...all)), min = Math.min(0, ...all) < 0 ? -nice(-Math.min(...all)) : 0;\n const y = (v: number) => top + ph - ((v - min) / (max - min)) * ph;\n for (let i = 0; i <= 4; i++) {\n const v = min + ((max - min) * i) / 4, yy = y(v);\n parts.push(`<line x1=\"${left}\" x2=\"${W - right}\" y1=\"${yy.toFixed(1)}\" y2=\"${yy.toFixed(1)}\" stroke=\"currentColor\" stroke-opacity=\".15\"/><text x=\"${left - 6}\" y=\"${(yy + 4).toFixed(1)}\" text-anchor=\"end\" fill=\"currentColor\" fill-opacity=\".7\">${fmt(v)}</text>`);\n }\n const n = spec.labels.length, slot = pw / n;\n spec.labels.forEach((l, i) => { if (n <= 12 || i % Math.ceil(n / 12) === 0) parts.push(`<text x=\"${(left + slot * (i + 0.5)).toFixed(1)}\" y=\"${top + ph + 16}\" text-anchor=\"middle\" fill=\"currentColor\" fill-opacity=\".8\">${esc(l.length > 12 ? `${l.slice(0, 11)}…` : l)}</text>`); });\n if (spec.type === 'bar') {\n const bw = Math.min(40, (slot * 0.76) / spec.series.length);\n spec.series.forEach((s, si) => s.values.forEach((v, i) => {\n const x = left + slot * (i + 0.5) - (bw * spec.series.length) / 2 + bw * si;\n parts.push(`<rect x=\"${x.toFixed(1)}\" y=\"${Math.min(y(v), y(0)).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${Math.abs(y(v) - y(0)).toFixed(1)}\" rx=\"2\" fill=\"${color(si)}\"><title>${esc(s.name)} · ${esc(spec.labels[i])}: ${v}</title></rect>`);\n }));\n } else {\n spec.series.forEach((s, si) => {\n const pts = s.values.map((v, i) => `${(left + slot * (i + 0.5)).toFixed(1)},${y(v).toFixed(1)}`);\n parts.push(`<polyline points=\"${pts.join(' ')}\" fill=\"none\" stroke=\"${color(si)}\" stroke-width=\"2.5\" stroke-linejoin=\"round\"/>`);\n s.values.forEach((v, i) => parts.push(`<circle cx=\"${(left + slot * (i + 0.5)).toFixed(1)}\" cy=\"${y(v).toFixed(1)}\" r=\"3.5\" fill=\"${color(si)}\"><title>${esc(s.name)} · ${esc(spec.labels[i])}: ${v}</title></circle>`));\n });\n }\n parts.push(`<line x1=\"${left}\" x2=\"${W - right}\" y1=\"${y(0).toFixed(1)}\" y2=\"${y(0).toFixed(1)}\" stroke=\"currentColor\" stroke-opacity=\".5\"/>`);\n if (legendH) parts.push(legend(spec.series.map((s) => s.name)));\n return `${head}${parts.join('')}</svg>`;\n}\n"],"mappings":"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"}
package/dist/collab.js CHANGED
@@ -1,4 +1,5 @@
1
- import { n as avatar } from "./dialog-DVjlHRqv.js";
1
+ import { n as avatar } from "./dialog-CBVhuASJ.js";
2
+ import { n as normalizeColor } from "./colors-B3cH2hYo.js";
2
3
  import { keymap } from "prosemirror-keymap";
3
4
  import { Plugin } from "prosemirror-state";
4
5
  import { redoCommand, relativePositionToAbsolutePosition, undoCommand, yCursorPlugin, ySyncPlugin, ySyncPluginKey, yUndoPlugin, yUndoPluginKey } from "y-prosemirror";
@@ -8,7 +9,7 @@ import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, removeAwareness
8
9
  import * as decoding from "lib0/decoding";
9
10
  import * as encoding from "lib0/encoding";
10
11
  import * as syncProtocol from "y-protocols/sync";
11
- //#region src/plugins/collaboration.ts
12
+ // src/plugins/collaboration.ts
12
13
  /** Real-time co-editing over Yjs. Undo/redo become per-user (they only undo your own changes). */
13
14
  function Collaboration(options) {
14
15
  const fragment = options.ydoc.getXmlFragment(options.field ?? "prosemirror");
@@ -92,8 +93,9 @@ function presenceView(editor, awareness) {
92
93
  b.setAttribute("aria-label", b.title);
93
94
  b.disabled = p.me || !p.has;
94
95
  const a = avatar(p.name, 26);
95
- a.style.background = p.color;
96
- a.style.boxShadow = `0 0 0 2px var(--wy-bg, #fff), 0 0 0 4px ${p.color}`;
96
+ const color = normalizeColor(String(p.color)) ?? "#6b7280";
97
+ a.style.background = color;
98
+ a.style.boxShadow = `0 0 0 2px var(--wy-bg, #fff), 0 0 0 4px ${color}`;
97
99
  b.append(a);
98
100
  b.addEventListener("click", () => jump(p.id));
99
101
  return b;
@@ -110,8 +112,7 @@ function presenceView(editor, awareness) {
110
112
  } };
111
113
  } });
112
114
  }
113
- //#endregion
114
- //#region src/collab-providers.ts
115
+ // src/collab-providers.ts
115
116
  /** Connect two documents in-process (tests, demos). Returns a function that disconnects them. */
116
117
  function linkDocs(a, b) {
117
118
  const toB = (u, origin) => origin !== "link" && Y.applyUpdate(b, u, "link");
@@ -269,7 +270,6 @@ function createWebSocketProvider(url, ydoc, options = {}) {
269
270
  };
270
271
  return provider;
271
272
  }
272
- //#endregion
273
273
  export { Collaboration, createBroadcastProvider, createWebSocketProvider, linkAwareness, linkDocs };
274
274
 
275
275
  //# sourceMappingURL=collab.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"collab.js","names":[],"sources":["../src/plugins/collaboration.ts","../src/collab-providers.ts"],"sourcesContent":["import { keymap } from 'prosemirror-keymap';\nimport { redoCommand, relativePositionToAbsolutePosition, undoCommand, yCursorPlugin, ySyncPlugin, ySyncPluginKey, yUndoPlugin, yUndoPluginKey } from 'y-prosemirror';\nimport { createRelativePositionFromJSON } from 'yjs';\nimport { avatar } from '../dialog';\nimport { Plugin } from 'prosemirror-state';\nimport type { Awareness } from 'y-protocols/awareness';\nimport type * as Y from 'yjs';\nimport type { EditorPlugin } from '../types';\n\nexport interface CollaborationOptions {\n ydoc: Y.Doc;\n /** Name of the shared XML fragment holding the document. Default 'prosemirror'. */\n field?: string;\n /** Pass an Awareness to show remote cursors and selections. */\n awareness?: Awareness;\n user?: { name: string; color: string };\n /**\n * HTML to put in the shared document if it is empty when the editor is created. With a network\n * provider, only enable this on the client that creates the document, or wait until it has synced.\n */\n seed?: string;\n /**\n * Show who is in the document: a row of avatars (needs `awareness`). Click one to scroll to where that person is working.\n * Default true when `awareness` is given.\n */\n presence?: boolean;\n}\n\n/** Real-time co-editing over Yjs. Undo/redo become per-user (they only undo your own changes). */\nexport function Collaboration(options: CollaborationOptions): EditorPlugin & { ydoc: Y.Doc } {\n const fragment = options.ydoc.getXmlFragment(options.field ?? 'prosemirror');\n return {\n name: 'collaboration',\n ydoc: options.ydoc, // lets the Comments plugin share its threads through the same document\n setup(editor) {\n options.awareness?.setLocalStateField('user', options.user ?? { name: 'Anonymous', color: '#2563eb' });\n editor.registerCommand('undo', (e) => undoCommand(e.view.state, e.view.dispatch) !== false);\n editor.registerCommand('redo', (e) => redoCommand(e.view.state, e.view.dispatch) !== false);\n return [\n ySyncPlugin(fragment),\n yUndoPlugin(),\n keymap({ 'Mod-z': undoCommand, 'Mod-y': redoCommand, 'Shift-Mod-z': redoCommand }),\n ...(options.awareness ? [yCursorPlugin(options.awareness)] : []),\n ...(options.awareness && options.presence !== false ? [presenceView(editor, options.awareness)] : []),\n new Plugin({\n view() {\n if (options.seed && fragment.length === 0) {\n setTimeout(() => {\n if (fragment.length !== 0) return;\n editor.replaceHTML(options.seed!);\n // The initial content must not be undoable.\n yUndoPluginKey.getState(editor.view.state)?.undoManager.clear();\n }, 0);\n }\n return {};\n },\n }),\n ];\n },\n };\n}\n\n\ninterface PeerState { user?: { name?: string; color?: string }; cursor?: { head?: unknown } | null }\n\n/** The \"who is here\" bar: one avatar per connected person, kept current from the awareness states. */\nfunction presenceView(editor: import('../editor').Editor, awareness: Awareness): Plugin {\n return new Plugin({\n view(view) {\n const bar = document.createElement('div');\n bar.className = 'wy-presence';\n bar.setAttribute('role', 'group');\n bar.setAttribute('aria-label', editor.t('presence', 'People in this document'));\n editor.root.insertBefore(bar, editor.body);\n\n /** Scroll to a person's cursor. Their position is a Yjs relative position; map it into this document first. */\n const jump = (clientId: number) => {\n const state = awareness.getStates().get(clientId) as PeerState | undefined;\n const head = state?.cursor?.head;\n const sync = ySyncPluginKey.getState(view.state);\n if (!head || !sync) return;\n try {\n const pos = relativePositionToAbsolutePosition(sync.doc, sync.type, createRelativePositionFromJSON(head as never), sync.binding.mapping);\n if (pos === null) return;\n const at = view.domAtPos(Math.min(pos, view.state.doc.content.size));\n const el = (at.node.nodeType === 1 ? at.node : at.node.parentElement) as HTMLElement | null;\n el?.scrollIntoView?.({ block: 'center', behavior: 'smooth' });\n el?.classList.add('wy-peer-flash');\n setTimeout(() => el?.classList.remove('wy-peer-flash'), 1200);\n } catch { /* the cursor points into text this editor has not received yet */ }\n };\n\n const render = () => {\n const people: { id: number; name: string; color: string; me: boolean; has: boolean }[] = [];\n awareness.getStates().forEach((s, id) => {\n const st = s as PeerState;\n if (!st.user?.name) return;\n people.push({ id, name: st.user.name, color: st.user.color ?? '#2563eb', me: id === awareness.clientID, has: !!st.cursor?.head });\n });\n people.sort((a, b) => Number(b.me) - Number(a.me) || a.name.localeCompare(b.name));\n bar.replaceChildren(...people.map((p) => {\n const b = document.createElement('button');\n b.type = 'button';\n b.className = `wy-peer${p.me ? ' is-me' : ''}`;\n b.title = p.me ? `${p.name} (you)` : p.has ? `${p.name}: go to their cursor` : p.name;\n b.setAttribute('aria-label', b.title);\n b.disabled = p.me || !p.has;\n const a = avatar(p.name, 26);\n a.style.background = p.color;\n a.style.boxShadow = `0 0 0 2px var(--wy-bg, #fff), 0 0 0 4px ${p.color}`;\n b.append(a);\n b.addEventListener('click', () => jump(p.id));\n return b;\n }));\n bar.hidden = people.length < 2; // alone in the document: nothing to show\n const count = people.length;\n bar.dataset.count = String(count);\n };\n awareness.on('change', render);\n render();\n return { destroy: () => { awareness.off('change', render); bar.remove(); } };\n },\n });\n}\n","import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness';\nimport * as decoding from 'lib0/decoding';\nimport * as encoding from 'lib0/encoding';\nimport * as syncProtocol from 'y-protocols/sync';\nimport * as Y from 'yjs';\n\n/** Connect two documents in-process (tests, demos). Returns a function that disconnects them. */\nexport function linkDocs(a: Y.Doc, b: Y.Doc): () => void {\n const toB = (u: Uint8Array, origin: unknown) => origin !== 'link' && Y.applyUpdate(b, u, 'link');\n const toA = (u: Uint8Array, origin: unknown) => origin !== 'link' && Y.applyUpdate(a, u, 'link');\n Y.applyUpdate(b, Y.encodeStateAsUpdate(a), 'link');\n Y.applyUpdate(a, Y.encodeStateAsUpdate(b), 'link');\n a.on('update', toB);\n b.on('update', toA);\n return () => (a.off('update', toB), b.off('update', toA));\n}\n\n/** Mirror two Awareness instances in-process, so two editors on one page show each other's cursors. */\nexport function linkAwareness(a: Awareness, b: Awareness): () => void {\n const forward = (from: Awareness, to: Awareness) => ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => {\n if (origin === 'link') return;\n applyAwarenessUpdate(to, encodeAwarenessUpdate(from, [...added, ...updated, ...removed]), 'link');\n };\n const ab = forward(a, b);\n const ba = forward(b, a);\n a.on('update', ab);\n b.on('update', ba);\n // Introduce them to each other's current state.\n applyAwarenessUpdate(b, encodeAwarenessUpdate(a, [...a.getStates().keys()]), 'link');\n applyAwarenessUpdate(a, encodeAwarenessUpdate(b, [...b.getStates().keys()]), 'link');\n return () => (a.off('update', ab), b.off('update', ba));\n}\n\ntype Msg =\n | { t: 'update'; d: Uint8Array }\n | { t: 'sv'; d: Uint8Array }\n | { t: 'awareness'; d: Uint8Array };\n\n/**\n * Sync a document (and cursors) between browser tabs of the same origin with BroadcastChannel.\n * No server needed; useful for demos and offline-first apps. Use a WebSocket provider across machines.\n */\nexport function createBroadcastProvider(room: string, ydoc: Y.Doc) {\n const channel = new BroadcastChannel(`wysiwyg-${room}`);\n const awareness = new Awareness(ydoc);\n const send = (m: Msg) => channel.postMessage(m);\n\n const onUpdate = (d: Uint8Array, origin: unknown) => origin !== 'broadcast' && send({ t: 'update', d });\n const onAwareness = ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => {\n if (origin === 'broadcast') return;\n send({ t: 'awareness', d: encodeAwarenessUpdate(awareness, [...added, ...updated, ...removed]) });\n };\n channel.onmessage = ({ data }: MessageEvent<Msg>) => {\n if (data.t === 'update') Y.applyUpdate(ydoc, data.d, 'broadcast');\n else if (data.t === 'sv') send({ t: 'update', d: Y.encodeStateAsUpdate(ydoc, data.d) }); // send what the peer is missing\n else applyAwarenessUpdate(awareness, data.d, 'broadcast');\n };\n ydoc.on('update', onUpdate);\n awareness.on('update', onAwareness);\n send({ t: 'sv', d: Y.encodeStateVector(ydoc) }); // ask peers for what we are missing\n send({ t: 'awareness', d: encodeAwarenessUpdate(awareness, [ydoc.clientID]) });\n\n return {\n awareness,\n destroy() {\n removeAwarenessStates(awareness, [ydoc.clientID], 'local');\n ydoc.off('update', onUpdate);\n awareness.off('update', onAwareness);\n channel.close();\n },\n };\n}\n\nexport type ProviderStatus = 'connecting' | 'connected' | 'disconnected' | 'denied';\n\nexport interface WebSocketProviderOptions {\n awareness?: Awareness;\n /** Inject a WebSocket implementation (Node tests: the `ws` package). Defaults to the global one. */\n WebSocketImpl?: typeof WebSocket;\n onStatus?: (s: ProviderStatus) => void;\n /** Reconnect with exponential backoff (max 10s). Default true. */\n reconnect?: boolean;\n}\n\n/**\n * Sync a document over a WebSocket speaking the Yjs sync + awareness protocols (the `server/` relay in this\n * repo, or y-websocket-compatible servers). `synced` resolves after the first full sync, which is the right\n * moment to seed an empty document.\n */\nexport function createWebSocketProvider(url: string, ydoc: Y.Doc, options: WebSocketProviderOptions = {}) {\n const awareness = options.awareness ?? new Awareness(ydoc);\n const WS = options.WebSocketImpl ?? WebSocket;\n let ws: WebSocket | null = null;\n let closed = false;\n let attempt = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let resolveSynced!: () => void;\n const synced = new Promise<void>((r) => (resolveSynced = r));\n const status = (s: ProviderStatus) => options.onStatus?.(s);\n\n const frame = (type: number, write: (e: encoding.Encoder) => void) => {\n const e = encoding.createEncoder();\n encoding.writeVarUint(e, type);\n write(e);\n return encoding.toUint8Array(e);\n };\n const sendFrame = (data: Uint8Array) => ws?.readyState === 1 && ws.send(data as Uint8Array<ArrayBuffer>);\n\n const onDocUpdate = (u: Uint8Array, origin: unknown) => origin !== provider && sendFrame(frame(0, (e) => syncProtocol.writeUpdate(e, u)));\n const onAwareness = ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => {\n if (origin !== provider) sendFrame(frame(1, (e) => encoding.writeVarUint8Array(e, encodeAwarenessUpdate(awareness, [...added, ...updated, ...removed]))));\n };\n\n const provider = { awareness, synced, destroy() {} };\n\n function connect() {\n status('connecting');\n ws = new WS(url);\n ws.binaryType = 'arraybuffer';\n ws.onopen = () => {\n attempt = 0;\n status('connected');\n sendFrame(frame(0, (e) => syncProtocol.writeSyncStep1(e, ydoc)));\n if (awareness.getLocalState() !== null) sendFrame(frame(1, (e) => encoding.writeVarUint8Array(e, encodeAwarenessUpdate(awareness, [ydoc.clientID]))));\n };\n ws.onmessage = (ev: MessageEvent) => {\n const dec = decoding.createDecoder(new Uint8Array(ev.data as ArrayBuffer));\n const type = decoding.readVarUint(dec);\n if (type === 0) {\n const reply = encoding.createEncoder();\n encoding.writeVarUint(reply, 0);\n const kind = syncProtocol.readSyncMessage(dec, reply, ydoc, provider);\n if (encoding.length(reply) > 1) sendFrame(encoding.toUint8Array(reply));\n if (kind === syncProtocol.messageYjsSyncStep2) resolveSynced();\n } else if (type === 1) {\n applyAwarenessUpdate(awareness, decoding.readVarUint8Array(dec), provider);\n }\n };\n ws.onclose = (ev: CloseEvent) => {\n removeAwarenessStates(awareness, [...awareness.getStates().keys()].filter((c) => c !== ydoc.clientID), provider);\n if (closed) return;\n if (ev.code === 4403 || ev.code === 4401) return status('denied'); // revoked: retrying would not help\n status('disconnected');\n if (options.reconnect !== false) timer = setTimeout(connect, Math.min(10000, 250 * 2 ** attempt++));\n };\n ws.onerror = () => {}; // `onclose` follows and handles reconnecting\n }\n\n ydoc.on('update', onDocUpdate);\n awareness.on('update', onAwareness);\n connect();\n provider.destroy = () => {\n closed = true;\n clearTimeout(timer);\n ydoc.off('update', onDocUpdate);\n awareness.off('update', onAwareness);\n removeAwarenessStates(awareness, [ydoc.clientID], provider);\n ws?.close();\n };\n return provider;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"collab.js","names":[],"sources":["../src/plugins/collaboration.ts","../src/collab-providers.ts"],"sourcesContent":["import { keymap } from 'prosemirror-keymap';\nimport { redoCommand, relativePositionToAbsolutePosition, undoCommand, yCursorPlugin, ySyncPlugin, ySyncPluginKey, yUndoPlugin, yUndoPluginKey } from 'y-prosemirror';\nimport { createRelativePositionFromJSON } from 'yjs';\nimport { avatar } from '../dialog';\nimport { normalizeColor } from './colors';\nimport { Plugin } from 'prosemirror-state';\nimport type { Awareness } from 'y-protocols/awareness';\nimport type * as Y from 'yjs';\nimport type { EditorPlugin } from '../types';\n\nexport interface CollaborationOptions {\n ydoc: Y.Doc;\n /** Name of the shared XML fragment holding the document. Default 'prosemirror'. */\n field?: string;\n /** Pass an Awareness to show remote cursors and selections. */\n awareness?: Awareness;\n user?: { name: string; color: string };\n /**\n * HTML to put in the shared document if it is empty when the editor is created. With a network\n * provider, only enable this on the client that creates the document, or wait until it has synced.\n */\n seed?: string;\n /**\n * Show who is in the document: a row of avatars (needs `awareness`). Click one to scroll to where that person is working.\n * Default true when `awareness` is given.\n */\n presence?: boolean;\n}\n\n/** Real-time co-editing over Yjs. Undo/redo become per-user (they only undo your own changes). */\nexport function Collaboration(options: CollaborationOptions): EditorPlugin & { ydoc: Y.Doc } {\n const fragment = options.ydoc.getXmlFragment(options.field ?? 'prosemirror');\n return {\n name: 'collaboration',\n ydoc: options.ydoc, // lets the Comments plugin share its threads through the same document\n setup(editor) {\n options.awareness?.setLocalStateField('user', options.user ?? { name: 'Anonymous', color: '#2563eb' });\n editor.registerCommand('undo', (e) => undoCommand(e.view.state, e.view.dispatch) !== false);\n editor.registerCommand('redo', (e) => redoCommand(e.view.state, e.view.dispatch) !== false);\n return [\n ySyncPlugin(fragment),\n yUndoPlugin(),\n keymap({ 'Mod-z': undoCommand, 'Mod-y': redoCommand, 'Shift-Mod-z': redoCommand }),\n ...(options.awareness ? [yCursorPlugin(options.awareness)] : []),\n ...(options.awareness && options.presence !== false ? [presenceView(editor, options.awareness)] : []),\n new Plugin({\n view() {\n if (options.seed && fragment.length === 0) {\n setTimeout(() => {\n if (fragment.length !== 0) return;\n editor.replaceHTML(options.seed!);\n // The initial content must not be undoable.\n yUndoPluginKey.getState(editor.view.state)?.undoManager.clear();\n }, 0);\n }\n return {};\n },\n }),\n ];\n },\n };\n}\n\n\ninterface PeerState { user?: { name?: string; color?: string }; cursor?: { head?: unknown } | null }\n\n/** The \"who is here\" bar: one avatar per connected person, kept current from the awareness states. */\nfunction presenceView(editor: import('../editor').Editor, awareness: Awareness): Plugin {\n return new Plugin({\n view(view) {\n const bar = document.createElement('div');\n bar.className = 'wy-presence';\n bar.setAttribute('role', 'group');\n bar.setAttribute('aria-label', editor.t('presence', 'People in this document'));\n editor.root.insertBefore(bar, editor.body);\n\n /** Scroll to a person's cursor. Their position is a Yjs relative position; map it into this document first. */\n const jump = (clientId: number) => {\n const state = awareness.getStates().get(clientId) as PeerState | undefined;\n const head = state?.cursor?.head;\n const sync = ySyncPluginKey.getState(view.state);\n if (!head || !sync) return;\n try {\n const pos = relativePositionToAbsolutePosition(sync.doc, sync.type, createRelativePositionFromJSON(head as never), sync.binding.mapping);\n if (pos === null) return;\n const at = view.domAtPos(Math.min(pos, view.state.doc.content.size));\n const el = (at.node.nodeType === 1 ? at.node : at.node.parentElement) as HTMLElement | null;\n el?.scrollIntoView?.({ block: 'center', behavior: 'smooth' });\n el?.classList.add('wy-peer-flash');\n setTimeout(() => el?.classList.remove('wy-peer-flash'), 1200);\n } catch { /* the cursor points into text this editor has not received yet */ }\n };\n\n const render = () => {\n const people: { id: number; name: string; color: string; me: boolean; has: boolean }[] = [];\n awareness.getStates().forEach((s, id) => {\n const st = s as PeerState;\n if (!st.user?.name) return;\n people.push({ id, name: st.user.name, color: st.user.color ?? '#2563eb', me: id === awareness.clientID, has: !!st.cursor?.head });\n });\n people.sort((a, b) => Number(b.me) - Number(a.me) || a.name.localeCompare(b.name));\n bar.replaceChildren(...people.map((p) => {\n const b = document.createElement('button');\n b.type = 'button';\n b.className = `wy-peer${p.me ? ' is-me' : ''}`;\n b.title = p.me ? `${p.name} (you)` : p.has ? `${p.name}: go to their cursor` : p.name;\n b.setAttribute('aria-label', b.title);\n b.disabled = p.me || !p.has;\n const a = avatar(p.name, 26);\n const color = normalizeColor(String(p.color)) ?? '#6b7280'; // remote data: only a plain colour may reach a style\n a.style.background = color;\n a.style.boxShadow = `0 0 0 2px var(--wy-bg, #fff), 0 0 0 4px ${color}`;\n b.append(a);\n b.addEventListener('click', () => jump(p.id));\n return b;\n }));\n bar.hidden = people.length < 2; // alone in the document: nothing to show\n const count = people.length;\n bar.dataset.count = String(count);\n };\n awareness.on('change', render);\n render();\n return { destroy: () => { awareness.off('change', render); bar.remove(); } };\n },\n });\n}\n","import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness';\nimport * as decoding from 'lib0/decoding';\nimport * as encoding from 'lib0/encoding';\nimport * as syncProtocol from 'y-protocols/sync';\nimport * as Y from 'yjs';\n\n/** Connect two documents in-process (tests, demos). Returns a function that disconnects them. */\nexport function linkDocs(a: Y.Doc, b: Y.Doc): () => void {\n const toB = (u: Uint8Array, origin: unknown) => origin !== 'link' && Y.applyUpdate(b, u, 'link');\n const toA = (u: Uint8Array, origin: unknown) => origin !== 'link' && Y.applyUpdate(a, u, 'link');\n Y.applyUpdate(b, Y.encodeStateAsUpdate(a), 'link');\n Y.applyUpdate(a, Y.encodeStateAsUpdate(b), 'link');\n a.on('update', toB);\n b.on('update', toA);\n return () => (a.off('update', toB), b.off('update', toA));\n}\n\n/** Mirror two Awareness instances in-process, so two editors on one page show each other's cursors. */\nexport function linkAwareness(a: Awareness, b: Awareness): () => void {\n const forward = (from: Awareness, to: Awareness) => ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => {\n if (origin === 'link') return;\n applyAwarenessUpdate(to, encodeAwarenessUpdate(from, [...added, ...updated, ...removed]), 'link');\n };\n const ab = forward(a, b);\n const ba = forward(b, a);\n a.on('update', ab);\n b.on('update', ba);\n // Introduce them to each other's current state.\n applyAwarenessUpdate(b, encodeAwarenessUpdate(a, [...a.getStates().keys()]), 'link');\n applyAwarenessUpdate(a, encodeAwarenessUpdate(b, [...b.getStates().keys()]), 'link');\n return () => (a.off('update', ab), b.off('update', ba));\n}\n\ntype Msg =\n | { t: 'update'; d: Uint8Array }\n | { t: 'sv'; d: Uint8Array }\n | { t: 'awareness'; d: Uint8Array };\n\n/**\n * Sync a document (and cursors) between browser tabs of the same origin with BroadcastChannel.\n * No server needed; useful for demos and offline-first apps. Use a WebSocket provider across machines.\n */\nexport function createBroadcastProvider(room: string, ydoc: Y.Doc) {\n const channel = new BroadcastChannel(`wysiwyg-${room}`);\n const awareness = new Awareness(ydoc);\n const send = (m: Msg) => channel.postMessage(m);\n\n const onUpdate = (d: Uint8Array, origin: unknown) => origin !== 'broadcast' && send({ t: 'update', d });\n const onAwareness = ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => {\n if (origin === 'broadcast') return;\n send({ t: 'awareness', d: encodeAwarenessUpdate(awareness, [...added, ...updated, ...removed]) });\n };\n channel.onmessage = ({ data }: MessageEvent<Msg>) => {\n if (data.t === 'update') Y.applyUpdate(ydoc, data.d, 'broadcast');\n else if (data.t === 'sv') send({ t: 'update', d: Y.encodeStateAsUpdate(ydoc, data.d) }); // send what the peer is missing\n else applyAwarenessUpdate(awareness, data.d, 'broadcast');\n };\n ydoc.on('update', onUpdate);\n awareness.on('update', onAwareness);\n send({ t: 'sv', d: Y.encodeStateVector(ydoc) }); // ask peers for what we are missing\n send({ t: 'awareness', d: encodeAwarenessUpdate(awareness, [ydoc.clientID]) });\n\n return {\n awareness,\n destroy() {\n removeAwarenessStates(awareness, [ydoc.clientID], 'local');\n ydoc.off('update', onUpdate);\n awareness.off('update', onAwareness);\n channel.close();\n },\n };\n}\n\nexport type ProviderStatus = 'connecting' | 'connected' | 'disconnected' | 'denied';\n\nexport interface WebSocketProviderOptions {\n awareness?: Awareness;\n /** Inject a WebSocket implementation (Node tests: the `ws` package). Defaults to the global one. */\n WebSocketImpl?: typeof WebSocket;\n onStatus?: (s: ProviderStatus) => void;\n /** Reconnect with exponential backoff (max 10s). Default true. */\n reconnect?: boolean;\n}\n\n/**\n * Sync a document over a WebSocket speaking the Yjs sync + awareness protocols (the `server/` relay in this\n * repo, or y-websocket-compatible servers). `synced` resolves after the first full sync, which is the right\n * moment to seed an empty document.\n */\nexport function createWebSocketProvider(url: string, ydoc: Y.Doc, options: WebSocketProviderOptions = {}) {\n const awareness = options.awareness ?? new Awareness(ydoc);\n const WS = options.WebSocketImpl ?? WebSocket;\n let ws: WebSocket | null = null;\n let closed = false;\n let attempt = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let resolveSynced!: () => void;\n const synced = new Promise<void>((r) => (resolveSynced = r));\n const status = (s: ProviderStatus) => options.onStatus?.(s);\n\n const frame = (type: number, write: (e: encoding.Encoder) => void) => {\n const e = encoding.createEncoder();\n encoding.writeVarUint(e, type);\n write(e);\n return encoding.toUint8Array(e);\n };\n const sendFrame = (data: Uint8Array) => ws?.readyState === 1 && ws.send(data as Uint8Array<ArrayBuffer>);\n\n const onDocUpdate = (u: Uint8Array, origin: unknown) => origin !== provider && sendFrame(frame(0, (e) => syncProtocol.writeUpdate(e, u)));\n const onAwareness = ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }, origin: unknown) => {\n if (origin !== provider) sendFrame(frame(1, (e) => encoding.writeVarUint8Array(e, encodeAwarenessUpdate(awareness, [...added, ...updated, ...removed]))));\n };\n\n const provider = { awareness, synced, destroy() {} };\n\n function connect() {\n status('connecting');\n ws = new WS(url);\n ws.binaryType = 'arraybuffer';\n ws.onopen = () => {\n attempt = 0;\n status('connected');\n sendFrame(frame(0, (e) => syncProtocol.writeSyncStep1(e, ydoc)));\n if (awareness.getLocalState() !== null) sendFrame(frame(1, (e) => encoding.writeVarUint8Array(e, encodeAwarenessUpdate(awareness, [ydoc.clientID]))));\n };\n ws.onmessage = (ev: MessageEvent) => {\n const dec = decoding.createDecoder(new Uint8Array(ev.data as ArrayBuffer));\n const type = decoding.readVarUint(dec);\n if (type === 0) {\n const reply = encoding.createEncoder();\n encoding.writeVarUint(reply, 0);\n const kind = syncProtocol.readSyncMessage(dec, reply, ydoc, provider);\n if (encoding.length(reply) > 1) sendFrame(encoding.toUint8Array(reply));\n if (kind === syncProtocol.messageYjsSyncStep2) resolveSynced();\n } else if (type === 1) {\n applyAwarenessUpdate(awareness, decoding.readVarUint8Array(dec), provider);\n }\n };\n ws.onclose = (ev: CloseEvent) => {\n removeAwarenessStates(awareness, [...awareness.getStates().keys()].filter((c) => c !== ydoc.clientID), provider);\n if (closed) return;\n if (ev.code === 4403 || ev.code === 4401) return status('denied'); // revoked: retrying would not help\n status('disconnected');\n if (options.reconnect !== false) timer = setTimeout(connect, Math.min(10000, 250 * 2 ** attempt++));\n };\n ws.onerror = () => {}; // `onclose` follows and handles reconnecting\n }\n\n ydoc.on('update', onDocUpdate);\n awareness.on('update', onAwareness);\n connect();\n provider.destroy = () => {\n closed = true;\n clearTimeout(timer);\n ydoc.off('update', onDocUpdate);\n awareness.off('update', onAwareness);\n removeAwarenessStates(awareness, [ydoc.clientID], provider);\n ws?.close();\n };\n return provider;\n}\n"],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ import { toggleMark } from "prosemirror-commands";
2
+ // src/plugins/colors.ts
3
+ var HEX = /^#[0-9a-f]{3,8}$/i;
4
+ var RGB = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/i;
5
+ /** Browsers normalise inline colors to rgb(); accept both and store hex. */
6
+ function normalizeColor(v) {
7
+ if (HEX.test(v)) return v;
8
+ const m = RGB.exec(v);
9
+ if (!m) return null;
10
+ return "#" + [
11
+ m[1],
12
+ m[2],
13
+ m[3]
14
+ ].map((n) => Math.min(255, +n).toString(16).padStart(2, "0")).join("");
15
+ }
16
+ var textColors = [
17
+ "#000000",
18
+ "#e03131",
19
+ "#2f9e44",
20
+ "#1971c2",
21
+ "#f08c00",
22
+ "#7048e8"
23
+ ];
24
+ var highlights = [
25
+ "#fff3bf",
26
+ "#ffc9c9",
27
+ "#b2f2bb",
28
+ "#a5d8ff",
29
+ "#d0bfff"
30
+ ];
31
+ function markValue(state, type) {
32
+ const { $from, from, to, empty } = state.selection;
33
+ let found = (empty ? state.storedMarks || $from.marks() : []).find((m) => m.type === type);
34
+ if (!found && !empty) state.doc.nodesBetween(from, to, (n) => void (found ||= type.isInSet(n.marks) || void 0));
35
+ return found?.attrs.color ?? "";
36
+ }
37
+ function colorMark(css) {
38
+ return {
39
+ attrs: { color: {} },
40
+ parseDOM: [{
41
+ style: css,
42
+ getAttrs: (v) => {
43
+ const color = normalizeColor(v);
44
+ return color ? { color } : false;
45
+ }
46
+ }],
47
+ toDOM: (m) => {
48
+ const c = normalizeColor(String(m.attrs.color));
49
+ return [
50
+ "span",
51
+ c ? { style: `${css}: ${c}` } : {},
52
+ 0
53
+ ];
54
+ }
55
+ };
56
+ }
57
+ /** Text color and highlight, chosen from a fixed palette. */
58
+ var Colors = {
59
+ name: "colors",
60
+ marks: {
61
+ text_color: colorMark("color"),
62
+ highlight: colorMark("background-color")
63
+ },
64
+ setup(editor) {
65
+ const apply = (markName) => (e, value) => {
66
+ const type = e.schema.marks[markName];
67
+ const { state, dispatch } = e.view;
68
+ const { from, to, empty } = state.selection;
69
+ if (!empty) dispatch(state.tr.removeMark(from, to, type));
70
+ else dispatch(state.tr.removeStoredMark(type));
71
+ if (!value) return true;
72
+ return toggleMark(type, { color: value })(e.view.state, e.view.dispatch);
73
+ };
74
+ editor.registerCommand("textColor", apply("text_color"));
75
+ editor.registerCommand("highlight", apply("highlight"));
76
+ },
77
+ toolbar: [{
78
+ type: "select",
79
+ name: "textColor",
80
+ label: "Text color",
81
+ command: "textColor",
82
+ options: [{
83
+ label: "Color",
84
+ value: ""
85
+ }, ...textColors.map((c) => ({
86
+ label: `● ${c}`,
87
+ value: c
88
+ }))],
89
+ getValue: (s) => markValue(s, s.schema.marks.text_color)
90
+ }, {
91
+ type: "select",
92
+ name: "highlight",
93
+ label: "Highlight",
94
+ command: "highlight",
95
+ options: [{
96
+ label: "Highlight",
97
+ value: ""
98
+ }, ...highlights.map((c) => ({
99
+ label: `▇ ${c}`,
100
+ value: c
101
+ }))],
102
+ getValue: (s) => markValue(s, s.schema.marks.highlight)
103
+ }]
104
+ };
105
+ export { normalizeColor as n, Colors as t };
106
+
107
+ //# sourceMappingURL=colors-B3cH2hYo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors-B3cH2hYo.js","names":[],"sources":["../src/plugins/colors.ts"],"sourcesContent":["import { toggleMark } from 'prosemirror-commands';\nimport type { MarkType } from 'prosemirror-model';\nimport type { EditorState } from 'prosemirror-state';\nimport type { EditorPlugin } from '../types';\n\nconst HEX = /^#[0-9a-f]{3,8}$/i;\nconst RGB = /^rgba?\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})/i;\n\n/** Browsers normalise inline colors to rgb(); accept both and store hex. */\nexport function normalizeColor(v: string): string | null {\n if (HEX.test(v)) return v;\n const m = RGB.exec(v);\n if (!m) return null;\n return '#' + [m[1], m[2], m[3]].map((n) => Math.min(255, +n).toString(16).padStart(2, '0')).join('');\n}\nconst textColors = ['#000000', '#e03131', '#2f9e44', '#1971c2', '#f08c00', '#7048e8'];\nconst highlights = ['#fff3bf', '#ffc9c9', '#b2f2bb', '#a5d8ff', '#d0bfff'];\n\nfunction markValue(state: EditorState, type: MarkType): string {\n const { $from, from, to, empty } = state.selection;\n const marks = empty ? state.storedMarks || $from.marks() : [];\n let found = marks.find((m) => m.type === type);\n if (!found && !empty) state.doc.nodesBetween(from, to, (n) => void (found ||= type.isInSet(n.marks) || undefined));\n return found?.attrs.color ?? '';\n}\n\nfunction colorMark(css: string): import('prosemirror-model').MarkSpec {\n return {\n attrs: { color: {} },\n parseDOM: [\n {\n style: css,\n getAttrs: (v) => {\n const color = normalizeColor(v as string);\n return color ? { color } : false;\n },\n },\n ],\n toDOM: (m) => { const c = normalizeColor(String(m.attrs.color)); return ['span', c ? { style: `${css}: ${c}` } : {}, 0]; }, // re-checked: an attribute that came from a collaborator never parsed\n };\n}\n\n/** Text color and highlight, chosen from a fixed palette. */\nexport const Colors: EditorPlugin = {\n name: 'colors',\n marks: { text_color: colorMark('color'), highlight: colorMark('background-color') },\n setup(editor) {\n const apply = (markName: string) => (e: typeof editor, value: string) => {\n const type = e.schema.marks[markName];\n const { state, dispatch } = e.view;\n // Strip any existing mark first so colors replace rather than stack.\n const { from, to, empty } = state.selection;\n if (!empty) dispatch(state.tr.removeMark(from, to, type));\n else dispatch(state.tr.removeStoredMark(type));\n if (!value) return true;\n return toggleMark(type, { color: value })(e.view.state, e.view.dispatch);\n };\n editor.registerCommand('textColor', apply('text_color'));\n editor.registerCommand('highlight', apply('highlight'));\n },\n toolbar: [\n {\n type: 'select',\n name: 'textColor',\n label: 'Text color',\n command: 'textColor',\n options: [{ label: 'Color', value: '' }, ...textColors.map((c) => ({ label: `● ${c}`, value: c }))],\n getValue: (s) => markValue(s, s.schema.marks.text_color),\n },\n {\n type: 'select',\n name: 'highlight',\n label: 'Highlight',\n command: 'highlight',\n options: [{ label: 'Highlight', value: '' }, ...highlights.map((c) => ({ label: `▇ ${c}`, value: c }))],\n getValue: (s) => markValue(s, s.schema.marks.highlight),\n },\n ],\n};\n"],"mappings":";;AAKA,IAAM,MAAM;AACZ,IAAM,MAAM;;AAGZ,SAAgB,eAAe,GAA0B;CACvD,IAAI,IAAI,KAAK,CAAC,GAAG,OAAO;CACxB,MAAM,IAAI,IAAI,KAAK,CAAC;CACpB,IAAI,CAAC,GAAG,OAAO;CACf,OAAO,MAAM;EAAC,EAAE;EAAI,EAAE;EAAI,EAAE;CAAE,CAAC,CAAC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AACrG;AACA,IAAM,aAAa;CAAC;CAAW;CAAW;CAAW;CAAW;CAAW;AAAS;AACpF,IAAM,aAAa;CAAC;CAAW;CAAW;CAAW;CAAW;AAAS;AAEzE,SAAS,UAAU,OAAoB,MAAwB;CAC7D,MAAM,EAAE,OAAO,MAAM,IAAI,UAAU,MAAM;CAEzC,IAAI,SADU,QAAQ,MAAM,eAAe,MAAM,MAAM,IAAI,CAAC,EAAA,CAC1C,MAAM,MAAM,EAAE,SAAS,IAAI;CAC7C,IAAI,CAAC,SAAS,CAAC,OAAO,MAAM,IAAI,aAAa,MAAM,KAAK,MAAM,MAAM,UAAU,KAAK,QAAQ,EAAE,KAAK,KAAK,KAAA,EAAU;CACjH,OAAO,OAAO,MAAM,SAAS;AAC/B;AAEA,SAAS,UAAU,KAAmD;CACpE,OAAO;EACL,OAAO,EAAE,OAAO,CAAC,EAAE;EACnB,UAAU,CACR;GACE,OAAO;GACP,WAAW,MAAM;IACf,MAAM,QAAQ,eAAe,CAAW;IACxC,OAAO,QAAQ,EAAE,MAAM,IAAI;GAC7B;EACF,CACF;EACA,QAAQ,MAAM;GAAE,MAAM,IAAI,eAAe,OAAO,EAAE,MAAM,KAAK,CAAC;GAAG,OAAO;IAAC;IAAQ,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,IAAI,IAAI,CAAC;IAAG;GAAC;EAAG;CAC3H;AACF;;AAGA,IAAa,SAAuB;CAClC,MAAM;CACN,OAAO;EAAE,YAAY,UAAU,OAAO;EAAG,WAAW,UAAU,kBAAkB;CAAE;CAClF,MAAM,QAAQ;EACZ,MAAM,SAAS,cAAsB,GAAkB,UAAkB;GACvE,MAAM,OAAO,EAAE,OAAO,MAAM;GAC5B,MAAM,EAAE,OAAO,aAAa,EAAE;GAE9B,MAAM,EAAE,MAAM,IAAI,UAAU,MAAM;GAClC,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,WAAW,MAAM,IAAI,IAAI,CAAC;QACnD,SAAS,MAAM,GAAG,iBAAiB,IAAI,CAAC;GAC7C,IAAI,CAAC,OAAO,OAAO;GACnB,OAAO,WAAW,MAAM,EAAE,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ;EACzE;EACA,OAAO,gBAAgB,aAAa,MAAM,YAAY,CAAC;EACvD,OAAO,gBAAgB,aAAa,MAAM,WAAW,CAAC;CACxD;CACA,SAAS,CACP;EACE,MAAM;EACN,MAAM;EACN,OAAO;EACP,SAAS;EACT,SAAS,CAAC;GAAE,OAAO;GAAS,OAAO;EAAG,GAAG,GAAG,WAAW,KAAK,OAAO;GAAE,OAAO,KAAK;GAAK,OAAO;EAAE,EAAE,CAAC;EAClG,WAAW,MAAM,UAAU,GAAG,EAAE,OAAO,MAAM,UAAU;CACzD,GACA;EACE,MAAM;EACN,MAAM;EACN,OAAO;EACP,SAAS;EACT,SAAS,CAAC;GAAE,OAAO;GAAa,OAAO;EAAG,GAAG,GAAG,WAAW,KAAK,OAAO;GAAE,OAAO,KAAK;GAAK,OAAO;EAAE,EAAE,CAAC;EACtG,WAAW,MAAM,UAAU,GAAG,EAAE,OAAO,MAAM,SAAS;CACxD,CACF;AACF"}
@@ -1,4 +1,4 @@
1
- //#region src/data-table-formula.ts
1
+ // src/data-table-formula.ts
2
2
  var FormulaError = class extends Error {
3
3
  code;
4
4
  constructor(code) {
@@ -418,8 +418,7 @@ function checkFormula(src, names) {
418
418
  return problem;
419
419
  }
420
420
  var FORMULA_FUNCTIONS = [...KNOWN].sort();
421
- //#endregion
422
- //#region src/data-table.ts
421
+ // src/data-table.ts
423
422
  var LOOKS = [
424
423
  "red",
425
424
  "green",
@@ -849,7 +848,7 @@ function queryRows(source, view, locale) {
849
848
  const visible = cols.map((c, i) => c.hidden ? -1 : i).filter((i) => i >= 0);
850
849
  const tests = [];
851
850
  cols.forEach((c, i) => {
852
- const t = filterTest(c, view.filters[c.key] ?? "", locale);
851
+ const t = filterTest(c, Object.prototype.hasOwnProperty.call(view.filters, c.key) ? view.filters[c.key] : "", locale);
853
852
  if (t) tests.push({
854
853
  i,
855
854
  t
@@ -1439,7 +1438,6 @@ async function parseXlsx(input) {
1439
1438
  sheet: first.getAttribute("name") ?? "Sheet1"
1440
1439
  };
1441
1440
  }
1442
- //#endregion
1443
1441
  export { checkFormula as S, toCSV as _, PAGE_SIZES as a, withFormulas as b, coerceCell as c, normalizeRows as d, parseCSV as f, specFromRows as g, ruleLooks as h, MAX_ROWS as i, formatCell as l, queryRows as m, COLUMN_TYPES as n, aggregate as o, parseXlsx as p, LOOKS as r, cleanSpec as s, AGGREGATES as t, groupDisplay as u, toPrintHTML as v, FORMULA_FUNCTIONS as x, toXlsx as y };
1444
1442
 
1445
- //# sourceMappingURL=data-table-C972cOYM.js.map
1443
+ //# sourceMappingURL=data-table-CgmN4xc4.js.map