wysiwygido 1.2.1 → 1.4.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 (82) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +61 -4
  3. package/dist/adapter.js +2 -3
  4. package/dist/{chart-CnJDVHSA.js → chart-CEPd_jSz.js} +3 -133
  5. package/dist/chart-CEPd_jSz.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/crop-oLrgb-A3.js +140 -0
  11. package/dist/crop-oLrgb-A3.js.map +1 -0
  12. package/dist/{data-table-C972cOYM.js → data-table-C4bo747f.js} +22 -13
  13. package/dist/data-table-C4bo747f.js.map +1 -0
  14. package/dist/{data-table-app-CI6OXjcZ.js → data-table-app-BCG8N37n.js} +10 -312
  15. package/dist/data-table-app-BCG8N37n.js.map +1 -0
  16. package/dist/data-table.d.ts +13 -0
  17. package/dist/data-table.js +3 -2
  18. package/dist/{dialog-DVjlHRqv.js → dialog-CBVhuASJ.js} +3 -5
  19. package/dist/{dialog-DVjlHRqv.js.map → dialog-CBVhuASJ.js.map} +1 -1
  20. package/dist/docx.d.ts +5 -0
  21. package/dist/docx.js +106 -57
  22. package/dist/docx.js.map +1 -1
  23. package/dist/editor.d.ts +9 -0
  24. package/dist/epub.js +2 -3
  25. package/dist/epub.js.map +1 -1
  26. package/dist/floating-bar.d.ts +48 -0
  27. package/dist/formula-bar.d.ts +31 -0
  28. package/dist/formula-catalog.d.ts +8 -0
  29. package/dist/formula-core.d.ts +136 -0
  30. package/dist/formula-eval.d.ts +13 -0
  31. package/dist/formula-lib-data.d.ts +5 -0
  32. package/dist/formula-lib-math.d.ts +9 -0
  33. package/dist/formula-lib-text.d.ts +7 -0
  34. package/dist/formula-lib.d.ts +3 -0
  35. package/dist/grid-formulas-DcdcPnK7.js +4096 -0
  36. package/dist/grid-formulas-DcdcPnK7.js.map +1 -0
  37. package/dist/grid-formulas.d.ts +87 -0
  38. package/dist/icons-CLvSP8nf.js +166 -0
  39. package/dist/icons-CLvSP8nf.js.map +1 -0
  40. package/dist/index.d.ts +2 -0
  41. package/dist/inert.d.ts +6 -0
  42. package/dist/office-chrome-BTBP-K4h.js +787 -0
  43. package/dist/office-chrome-BTBP-K4h.js.map +1 -0
  44. package/dist/office-chrome.d.ts +147 -0
  45. package/dist/paste.d.ts +6 -6
  46. package/dist/pdf.js +1 -2
  47. package/dist/plugins/context-menu.d.ts +6 -0
  48. package/dist/plugins/find-replace.d.ts +9 -2
  49. package/dist/plugins/index.d.ts +2 -1
  50. package/dist/plugins/offline.d.ts +10 -0
  51. package/dist/plugins/tab-key.d.ts +11 -0
  52. package/dist/plugins/table-formulas.d.ts +4 -6
  53. package/dist/presentation-app.d.ts +81 -0
  54. package/dist/presentation-editor.d.ts +103 -0
  55. package/dist/presentation-model.d.ts +185 -0
  56. package/dist/presentation-pptx.d.ts +15 -0
  57. package/dist/presentation-render.d.ts +20 -0
  58. package/dist/presentation-show.d.ts +16 -0
  59. package/dist/presentation.js +5763 -0
  60. package/dist/presentation.js.map +1 -0
  61. package/dist/react.js +2 -3
  62. package/dist/resize.d.ts +5 -0
  63. package/dist/spreadsheet-app.d.ts +105 -0
  64. package/dist/spreadsheet-dialog.d.ts +39 -0
  65. package/dist/spreadsheet-features.d.ts +112 -0
  66. package/dist/spreadsheet-model.d.ts +300 -0
  67. package/dist/spreadsheet-view.d.ts +148 -0
  68. package/dist/spreadsheet.js +6991 -0
  69. package/dist/spreadsheet.js.map +1 -0
  70. package/dist/{src-BRq1hjtj.js → src-CYQYge8P.js} +3934 -3910
  71. package/dist/src-CYQYge8P.js.map +1 -0
  72. package/dist/style.css +356 -2
  73. package/dist/svelte.js +1 -2
  74. package/dist/themes-B7BDDWRQ.js +301 -0
  75. package/dist/themes-B7BDDWRQ.js.map +1 -0
  76. package/dist/vue.js +2 -3
  77. package/dist/wysiwygido.js +11 -6
  78. package/package.json +15 -7
  79. package/dist/chart-CnJDVHSA.js.map +0 -1
  80. package/dist/data-table-C972cOYM.js.map +0 -1
  81. package/dist/data-table-app-CI6OXjcZ.js.map +0 -1
  82. package/dist/src-BRq1hjtj.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,7 +2,25 @@
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
- ## Unreleased
5
+ ## 1.4.0 - 2026-10-25
6
+
7
+ - **Presentation: PowerPoint (.pptx) import and export.** File menu entries "Open .pptx…" and "Download as PowerPoint (.pptx)"; `toPptx()` and `loadPptx(file)`; an `import` event (`{ slides, skipped }`). Strict zip and XML reader (size, entry count and compression limits, no DOCTYPE or entities, path checks); everything read goes through `cleanDeck`.
8
+
9
+ - **Spreadsheet: merged cells, conditional formats and charts.** Merge, merge across and unmerge (the selection, the arrow keys, the editor and printing treat a merge as one cell); conditional formatting with highlight rules, top and bottom, above and below average, duplicates, blanks, errors, data bars, colour scales and formula rules, and a rules list; column, line and pie charts you can move, resize, edit and delete. All saved in the workbook, followed through inserted and deleted rows and columns, undoable, limited in number (2,000 merges, 100 rules and 50 charts per sheet) and checked when loaded. New API: `merge`, `unmerge`, `addConditionalFormat`, `clearConditionalFormats`, `addChart`, `removeChart`.
10
+
11
+ ## 1.3.0 - 2026-10-25
12
+
13
+ - **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`.
14
+ - **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.
15
+
16
+ - **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.
17
+ - **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.
18
+
19
+ - 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`.
20
+
21
+ - A numbered or bulleted heading now has the collapse arrow too (left of the number); it hides what follows inside its list item.
22
+
23
+ - 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.
6
24
 
7
25
  - 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.
8
26
  - 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.
package/README.md CHANGED
@@ -163,6 +163,10 @@ Markdown), so exports show it. A reference to a deleted caption says "Missing re
163
163
 
164
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
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
+
166
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.
167
171
 
168
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.
@@ -598,6 +602,52 @@ Rows can be arrays in column order or objects by column key (objects are easier
598
602
 
599
603
  Events `table-ready` and `table-change` (`detail.table`); the element's `table` property sets or reads the table.
600
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: defined names in the UI, `LAMBDA` and its helpers, complex-number, bond, cube and web functions (those give `#NAME?`), and pivot tables.
627
+
628
+ **Merged cells, conditional formats and charts.** Merge cells from the alignment group (Merge cells, Merge across, Unmerge); the top left value stays and the rest is emptied; selections and arrow keys treat a merge as one cell, and rows or columns inserted or deleted move it. Conditional formatting (Home, Styles) has highlight rules (greater than, less than, between, equal to, text that contains, duplicate or unique values, blanks, errors), top and bottom N or N percent, above and below average, data bars, two- and three-colour scales and formula rules, with a list to manage them. Charts (Insert, Chart) are columns, lines or pies drawn from a range (first row names the series, first column the labels); drag to move, drag the corner to resize, double-click to edit, Delete to remove. Everything is saved with the workbook, follows inserted rows and columns, is checked on load, and prints (charts after the table). From code: `sheet.merge('A1:C1')`, `sheet.addConditionalFormat('B2:B20', { type: 'cell', op: '>', a: '100', style: { bg: '#fecaca' } })`, `sheet.addChart({ range: 'A1:C13', type: 'line', title: 'Sales' })`. Copy and paste does not carry merges, rules or charts to another place.
629
+
630
+ ## Presentation
631
+
632
+ 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.
633
+
634
+ ```ts
635
+ import { createPresentation } from 'wysiwygido/presentation';
636
+ import 'wysiwygido/style.css';
637
+
638
+ const deck = createPresentation({
639
+ element,
640
+ menu: 'floating', // or 'classic': File, Home, Insert, Design, Transitions, Slide Show, View
641
+ data: { title: 'Q4 review', theme: 'ocean', slides: [{ layout: 'title', els: [], notes: '' }] },
642
+ save: (data) => fetch('/api/deck', { method: 'PUT', body: JSON.stringify(data) }),
643
+ });
644
+ deck.addSlide('titleContent');
645
+ deck.present(0); // deck.present(0, true) opens the presenter view
646
+ const page = deck.toHTML(); // everything in one file
647
+ ```
648
+
649
+ 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 files** open and save (File, Open .pptx / Download as PowerPoint, or `deck.loadPptx(file)` and `deck.toPptx()`): text boxes and shapes with their text formatting, bullets, links, pictures (png, jpeg, gif, webp on import; svg is drawn to png on export), notes, backgrounds, hidden slides and fade, slide and zoom transitions. Tables, charts, SmartArt, video, audio and animations are skipped and listed in `skipped` (and the `import` event). Gradients become their first colour, master text styles are not read, and a file is opened in a strict reader: no more than 2,000 parts or 150 MB unpacked, no entity declarations, and links only to `http(s)`, `mailto` and `tel`. Files written here read back faithfully and open in a strict parser (python-pptx); they have not been opened in PowerPoint, Keynote or Google Slides.
650
+
601
651
  ## Themes
602
652
 
603
653
  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.
@@ -676,10 +726,17 @@ Leave the plugin out of `plugins` to have no custom menu.
676
726
 
677
727
  ## Security notes
678
728
 
679
- Pasted/loaded HTML goes through the schema: unknown tags and attributes are dropped; link and image URLs are limited to
680
- safe schemes (`javascript:` is rejected); Markdown import disables raw HTML; user text (comments, mentions, footnotes, AI
681
- output) is rendered with `textContent`; the server compares secrets in constant time, stores only hashes, bounds request
682
- size and treats unknown documents and bad credentials identically.
729
+ **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.
730
+
731
+ **What stays with you.**
732
+ - *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.
733
+ - *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.
734
+ - *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.
735
+ - *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.
736
+ - *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).
737
+ - *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.
738
+
739
+ Report a vulnerability privately to the maintainer rather than in a public issue.
683
740
 
684
741
  ## License
685
742
 
package/dist/adapter.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as createEditor } from "./src-BRq1hjtj.js";
2
- //#region src/adapter.ts
1
+ import { t as createEditor } from "./src-CYQYge8P.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,133 +1,4 @@
1
- //#region src/crop.ts
2
- /** The smallest visible share of the image on either axis (so a crop can never collapse to nothing). */
3
- var MIN_VISIBLE = .05;
4
- var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
5
- var round = (v) => Math.round(v * 1e4) / 1e4;
6
- /** Clamp a crop into a valid range; null means "no crop". Junk values are treated as 0. */
7
- function normalizeCrop(input) {
8
- if (!input || typeof input !== "object") return null;
9
- const n = (v) => typeof v === "number" && Number.isFinite(v) ? clamp(v, 0, .95) : 0;
10
- let { left, top, right, bottom } = {
11
- left: n(input.left),
12
- top: n(input.top),
13
- right: n(input.right),
14
- bottom: n(input.bottom)
15
- };
16
- const fit = (a, b) => {
17
- const excess = a + b - .95;
18
- if (excess <= 0) return [a, b];
19
- return a >= b ? [a - excess, b] : [a, b - excess];
20
- };
21
- [left, right] = fit(left, right);
22
- [top, bottom] = fit(top, bottom);
23
- const c = {
24
- left: round(left),
25
- top: round(top),
26
- right: round(right),
27
- bottom: round(bottom)
28
- };
29
- return c.left || c.top || c.right || c.bottom ? c : null;
30
- }
31
- /** Natural size of the visible region (what a crop leaves), in natural px. */
32
- function visibleSize(crop, nat) {
33
- const c = crop ?? {
34
- left: 0,
35
- top: 0,
36
- right: 0,
37
- bottom: 0
38
- };
39
- return {
40
- w: nat.w * (1 - c.left - c.right),
41
- h: nat.h * (1 - c.top - c.bottom)
42
- };
43
- }
44
- /** Lay out a crop so the visible box is `displayWidth` px wide. */
45
- function cropLayout(crop, nat, displayWidth) {
46
- const vw = 1 - crop.left - crop.right;
47
- const vh = 1 - crop.top - crop.bottom;
48
- const width = displayWidth;
49
- const height = width * (vh * nat.h) / (vw * nat.w);
50
- const imgWidth = width / vw;
51
- const imgHeight = height / vh;
52
- return {
53
- width,
54
- height,
55
- imgWidth,
56
- imgHeight,
57
- offsetX: -crop.left * imgWidth,
58
- offsetY: -crop.top * imgHeight
59
- };
60
- }
61
- /** Serialize as "left,top,right,bottom" for a data attribute. */
62
- var cropToString = (c) => `${c.left},${c.top},${c.right},${c.bottom}`;
63
- function cropFromString(s) {
64
- if (!s) return null;
65
- const p = s.split(",").map(Number);
66
- if (p.length !== 4 || p.some((x) => !Number.isFinite(x) || x < 0 || x >= 1)) return null;
67
- return normalizeCrop({
68
- left: p[0],
69
- top: p[1],
70
- right: p[2],
71
- bottom: p[3]
72
- });
73
- }
74
- /**
75
- * Apply a drag of (dx, dy) fractions of the full image to a crop rectangle. Edges move independently,
76
- * `move` slides the whole rectangle without resizing it. Result is always valid.
77
- */
78
- function dragCrop(start, handle, dx, dy) {
79
- let { left, top, right, bottom } = start;
80
- if (handle === "move") {
81
- const w = 1 - left - right;
82
- const h = 1 - top - bottom;
83
- left = clamp(left + dx, 0, 1 - w);
84
- top = clamp(top + dy, 0, 1 - h);
85
- right = 1 - w - left;
86
- bottom = 1 - h - top;
87
- } else {
88
- if (handle.includes("w")) left = clamp(left + dx, 0, 1 - right - MIN_VISIBLE);
89
- if (handle.includes("e")) right = clamp(right - dx, 0, 1 - left - MIN_VISIBLE);
90
- if (handle.includes("n")) top = clamp(top + dy, 0, 1 - bottom - MIN_VISIBLE);
91
- if (handle.includes("s")) bottom = clamp(bottom - dy, 0, 1 - top - MIN_VISIBLE);
92
- }
93
- return {
94
- left: round(left),
95
- top: round(top),
96
- right: round(right),
97
- bottom: round(bottom)
98
- };
99
- }
100
- /**
101
- * Render the cropped region of an image to PNG bytes (browser only: needs canvas). Returns null when canvas
102
- * is unavailable, so callers can fall back to the full image.
103
- */
104
- async function cropBytes(data, crop) {
105
- try {
106
- if (typeof createImageBitmap !== "function") return null;
107
- const bmp = await createImageBitmap(new Blob([data]));
108
- const sx = Math.round(bmp.width * crop.left);
109
- const sy = Math.round(bmp.height * crop.top);
110
- const sw = Math.max(1, Math.round(bmp.width * (1 - crop.left - crop.right)));
111
- const sh = Math.max(1, Math.round(bmp.height * (1 - crop.top - crop.bottom)));
112
- const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(sw, sh) : Object.assign(document.createElement("canvas"), {
113
- width: sw,
114
- height: sh
115
- });
116
- const ctx = canvas.getContext("2d");
117
- if (!ctx) return null;
118
- ctx.drawImage(bmp, sx, sy, sw, sh, 0, 0, sw, sh);
119
- const blob = "convertToBlob" in canvas ? await canvas.convertToBlob({ type: "image/png" }) : await new Promise((r) => canvas.toBlob(r, "image/png"));
120
- return blob ? {
121
- data: new Uint8Array(await blob.arrayBuffer()),
122
- width: sw,
123
- height: sh
124
- } : null;
125
- } catch {
126
- return null;
127
- }
128
- }
129
- //#endregion
130
- //#region src/chart.ts
1
+ // src/chart.ts
131
2
  var TYPES = [
132
3
  "bar",
133
4
  "line",
@@ -274,7 +145,6 @@ function chartSVG(spec, width = 560, height = 320) {
274
145
  if (legendH) parts.push(legend(spec.series.map((s) => s.name)));
275
146
  return `${head}${parts.join("")}</svg>`;
276
147
  }
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 };
148
+ export { specFromRows as a, parseNumber as i, chartSVG as n, cleanChart as r, CHART_COLORS as t };
279
149
 
280
- //# sourceMappingURL=chart-CnJDVHSA.js.map
150
+ //# sourceMappingURL=chart-CEPd_jSz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chart-CEPd_jSz.js","names":[],"sources":["../src/chart.ts"],"sourcesContent":["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"}