From 7bbf591a7fdb46bb0da30669354b8768ac950506 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 20 Jul 2025 12:33:22 +0100 Subject: [PATCH 01/11] MD Editor: Starting conversion to typescript --- dev/build/esbuild.js | 2 +- lang/en/entities.php | 1 + ...ctor-popup.js => entity-selector-popup.ts} | 40 ++--- ...{entity-selector.js => entity-selector.ts} | 82 ++++----- resources/js/components/image-manager.js | 4 + .../js/markdown/{actions.js => actions.ts} | 161 ++++++++---------- resources/js/markdown/codemirror.js | 2 +- resources/js/markdown/common-events.js | 32 ---- resources/js/markdown/common-events.ts | 36 ++++ resources/js/markdown/index.mjs | 51 ------ resources/js/markdown/index.mts | 52 ++++++ resources/js/markdown/settings.js | 1 + .../pages/parts/markdown-editor.blade.php | 4 + 13 files changed, 238 insertions(+), 230 deletions(-) rename resources/js/components/{entity-selector-popup.js => entity-selector-popup.ts} (58%) rename resources/js/components/{entity-selector.js => entity-selector.ts} (74%) rename resources/js/markdown/{actions.js => actions.ts} (82%) delete mode 100644 resources/js/markdown/common-events.js create mode 100644 resources/js/markdown/common-events.ts delete mode 100644 resources/js/markdown/index.mjs create mode 100644 resources/js/markdown/index.mts diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js index cd8bf279f28..63387d612ce 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.js @@ -13,7 +13,7 @@ const entryPoints = { app: path.join(__dirname, '../../resources/js/app.ts'), code: path.join(__dirname, '../../resources/js/code/index.mjs'), 'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'), - markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'), + markdown: path.join(__dirname, '../../resources/js/markdown/index.mts'), wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.ts'), }; diff --git a/lang/en/entities.php b/lang/en/entities.php index 561022ad6b6..ef625a3d261 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -268,6 +268,7 @@ 'pages_md_insert_drawing' => 'Insert Drawing', 'pages_md_show_preview' => 'Show preview', 'pages_md_sync_scroll' => 'Sync preview scroll', + 'pages_md_plain_editor' => 'Plaintext editor', 'pages_drawing_unsaved' => 'Unsaved Drawing Found', 'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?', 'pages_not_in_chapter' => 'Page is not in a chapter', diff --git a/resources/js/components/entity-selector-popup.js b/resources/js/components/entity-selector-popup.ts similarity index 58% rename from resources/js/components/entity-selector-popup.js rename to resources/js/components/entity-selector-popup.ts index 29c06e90951..468f074b5d4 100644 --- a/resources/js/components/entity-selector-popup.js +++ b/resources/js/components/entity-selector-popup.ts @@ -1,15 +1,23 @@ import {Component} from './component'; +import {EntitySelector, EntitySelectorEntity, EntitySelectorSearchOptions} from "./entity-selector"; +import {Popup} from "./popup"; + +export type EntitySelectorPopupCallback = (entity: EntitySelectorEntity) => void; export class EntitySelectorPopup extends Component { + protected container!: HTMLElement; + protected selectButton!: HTMLElement; + protected selectorEl!: HTMLElement; + + protected callback: EntitySelectorPopupCallback|null = null; + protected selection: EntitySelectorEntity|null = null; + setup() { this.container = this.$el; this.selectButton = this.$refs.select; this.selectorEl = this.$refs.selector; - this.callback = null; - this.selection = null; - this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this)); window.$events.listen('entity-select-change', this.onSelectionChange.bind(this)); window.$events.listen('entity-select-confirm', this.handleConfirmedSelection.bind(this)); @@ -17,10 +25,8 @@ export class EntitySelectorPopup extends Component { /** * Show the selector popup. - * @param {Function} callback - * @param {EntitySelectorSearchOptions} searchOptions */ - show(callback, searchOptions = {}) { + show(callback: EntitySelectorPopupCallback, searchOptions: Partial = {}) { this.callback = callback; this.getSelector().configureSearchOptions(searchOptions); this.getPopup().show(); @@ -32,34 +38,28 @@ export class EntitySelectorPopup extends Component { this.getPopup().hide(); } - /** - * @returns {Popup} - */ - getPopup() { - return window.$components.firstOnElement(this.container, 'popup'); + getPopup(): Popup { + return window.$components.firstOnElement(this.container, 'popup') as Popup; } - /** - * @returns {EntitySelector} - */ - getSelector() { - return window.$components.firstOnElement(this.selectorEl, 'entity-selector'); + getSelector(): EntitySelector { + return window.$components.firstOnElement(this.selectorEl, 'entity-selector') as EntitySelector; } onSelectButtonClick() { this.handleConfirmedSelection(this.selection); } - onSelectionChange(entity) { - this.selection = entity; - if (entity === null) { + onSelectionChange(entity: EntitySelectorEntity|{}) { + this.selection = (entity.hasOwnProperty('id') ? entity : null) as EntitySelectorEntity|null; + if (!this.selection) { this.selectButton.setAttribute('disabled', 'true'); } else { this.selectButton.removeAttribute('disabled'); } } - handleConfirmedSelection(entity) { + handleConfirmedSelection(entity: EntitySelectorEntity|null): void { this.hide(); this.getSelector().reset(); if (this.callback && entity) this.callback(entity); diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.ts similarity index 74% rename from resources/js/components/entity-selector.js rename to resources/js/components/entity-selector.ts index 7491119a137..0ae9710f75e 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.ts @@ -1,24 +1,36 @@ -import {onChildEvent} from '../services/dom.ts'; +import {onChildEvent} from '../services/dom'; import {Component} from './component'; -/** - * @typedef EntitySelectorSearchOptions - * @property entityTypes string - * @property entityPermission string - * @property searchEndpoint string - * @property initialValue string - */ - -/** - * Entity Selector - */ +export interface EntitySelectorSearchOptions { + entityTypes: string; + entityPermission: string; + searchEndpoint: string; + initialValue: string; +} + +export type EntitySelectorEntity = { + id: number, + name: string, + link: string, +}; + export class EntitySelector extends Component { + protected elem!: HTMLElement; + protected input!: HTMLInputElement; + protected searchInput!: HTMLInputElement; + protected loading!: HTMLElement; + protected resultsContainer!: HTMLElement; + + protected searchOptions!: EntitySelectorSearchOptions; + + protected search = ''; + protected lastClick = 0; setup() { this.elem = this.$el; - this.input = this.$refs.input; - this.searchInput = this.$refs.search; + this.input = this.$refs.input as HTMLInputElement; + this.searchInput = this.$refs.search as HTMLInputElement; this.loading = this.$refs.loading; this.resultsContainer = this.$refs.results; @@ -29,9 +41,6 @@ export class EntitySelector extends Component { initialValue: this.searchInput.value || '', }; - this.search = ''; - this.lastClick = 0; - this.setupListeners(); this.showLoading(); @@ -40,16 +49,13 @@ export class EntitySelector extends Component { } } - /** - * @param {EntitySelectorSearchOptions} options - */ - configureSearchOptions(options) { + configureSearchOptions(options: Partial): void { Object.assign(this.searchOptions, options); this.reset(); this.searchInput.value = this.searchOptions.initialValue; } - setupListeners() { + setupListeners(): void { this.elem.addEventListener('click', this.onClick.bind(this)); let lastSearch = 0; @@ -67,7 +73,7 @@ export class EntitySelector extends Component { }); // Keyboard navigation - onChildEvent(this.$el, '[data-entity-type]', 'keydown', event => { + onChildEvent(this.$el, '[data-entity-type]', 'keydown', ((event: KeyboardEvent) => { if (event.ctrlKey && event.code === 'Enter') { const form = this.$el.closest('form'); if (form) { @@ -83,7 +89,7 @@ export class EntitySelector extends Component { if (event.code === 'ArrowUp') { this.focusAdjacent(false); } - }); + }) as (event: Event) => void); this.searchInput.addEventListener('keydown', event => { if (event.code === 'ArrowDown') { @@ -93,10 +99,10 @@ export class EntitySelector extends Component { } focusAdjacent(forward = true) { - const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]')); + const items: (Element|null)[] = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]')); const selectedIndex = items.indexOf(document.activeElement); const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0]; - if (newItem) { + if (newItem instanceof HTMLElement) { newItem.focus(); } } @@ -132,7 +138,7 @@ export class EntitySelector extends Component { } window.$http.get(this.searchUrl()).then(resp => { - this.resultsContainer.innerHTML = resp.data; + this.resultsContainer.innerHTML = resp.data as string; this.hideLoading(); }); } @@ -142,7 +148,7 @@ export class EntitySelector extends Component { return `${this.searchOptions.searchEndpoint}?${query}`; } - searchEntities(searchTerm) { + searchEntities(searchTerm: string) { if (!this.searchOptions.searchEndpoint) { throw new Error('Search endpoint not set for entity-selector load'); } @@ -150,7 +156,7 @@ export class EntitySelector extends Component { this.input.value = ''; const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`; window.$http.get(url).then(resp => { - this.resultsContainer.innerHTML = resp.data; + this.resultsContainer.innerHTML = resp.data as string; this.hideLoading(); }); } @@ -162,16 +168,16 @@ export class EntitySelector extends Component { return answer; } - onClick(event) { - const listItem = event.target.closest('[data-entity-type]'); - if (listItem) { + onClick(event: MouseEvent) { + const listItem = (event.target as HTMLElement).closest('[data-entity-type]'); + if (listItem instanceof HTMLElement) { event.preventDefault(); event.stopPropagation(); this.selectItem(listItem); } } - selectItem(item) { + selectItem(item: HTMLElement): void { const isDblClick = this.isDoubleClick(); const type = item.getAttribute('data-entity-type'); const id = item.getAttribute('data-entity-id'); @@ -180,14 +186,14 @@ export class EntitySelector extends Component { this.unselectAll(); this.input.value = isSelected ? `${type}:${id}` : ''; - const link = item.getAttribute('href'); - const name = item.querySelector('.entity-list-item-name').textContent; - const data = {id: Number(id), name, link}; + const link = item.getAttribute('href') || ''; + const name = item.querySelector('.entity-list-item-name')?.textContent || ''; + const data: EntitySelectorEntity = {id: Number(id), name, link}; if (isSelected) { item.classList.add('selected'); } else { - window.$events.emit('entity-select-change', null); + window.$events.emit('entity-select-change'); } if (!isDblClick && !isSelected) return; @@ -200,7 +206,7 @@ export class EntitySelector extends Component { } } - confirmSelection(data) { + confirmSelection(data: EntitySelectorEntity) { window.$events.emit('entity-select-confirm', data); } diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index c8108ab28c1..84ba333f9da 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -127,6 +127,10 @@ export class ImageManager extends Component { }); } + /** + * @param {({ thumbs: { display: string; }; url: string; name: string; }) => void} callback + * @param {String} type + */ show(callback, type = 'gallery') { this.resetAll(); diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.ts similarity index 82% rename from resources/js/markdown/actions.js rename to resources/js/markdown/actions.ts index e99bbf3e14f..c6210809cce 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.ts @@ -1,16 +1,25 @@ -import * as DrawIO from '../services/drawio.ts'; +import * as DrawIO from '../services/drawio'; +import {MarkdownEditor} from "./index.mjs"; +import {EntitySelectorPopup, ImageManager} from "../components"; +import {ChangeSpec, SelectionRange, TransactionSpec} from "@codemirror/state"; + +interface ImageManagerImage { + id: number; + name: string; + thumbs: { display: string; }; + url: string; +} export class Actions { - /** - * @param {MarkdownEditor} editor - */ - constructor(editor) { + protected readonly editor: MarkdownEditor; + protected lastContent: { html: string; markdown: string } = { + html: '', + markdown: '', + }; + + constructor(editor: MarkdownEditor) { this.editor = editor; - this.lastContent = { - html: '', - markdown: '', - }; } updateAndRender() { @@ -30,10 +39,9 @@ export class Actions { } showImageInsert() { - /** @type {ImageManager} * */ - const imageManager = window.$components.first('image-manager'); + const imageManager = window.$components.first('image-manager') as ImageManager; - imageManager.show(image => { + imageManager.show((image: ImageManagerImage) => { const imageUrl = image.thumbs?.display || image.url; const selectedText = this.#getSelectionText(); const newText = `[![${selectedText || image.name}](${imageUrl})](${image.url})`; @@ -55,9 +63,8 @@ export class Actions { showImageManager() { const selectionRange = this.#getSelectionRange(); - /** @type {ImageManager} * */ - const imageManager = window.$components.first('image-manager'); - imageManager.show(image => { + const imageManager = window.$components.first('image-manager') as ImageManager; + imageManager.show((image: ImageManagerImage) => { this.#insertDrawing(image, selectionRange); }, 'drawio'); } @@ -66,8 +73,7 @@ export class Actions { showLinkSelector() { const selectionRange = this.#getSelectionRange(); - /** @type {EntitySelectorPopup} * */ - const selector = window.$components.first('entity-selector-popup'); + const selector = window.$components.first('entity-selector-popup') as EntitySelectorPopup; const selectionText = this.#getSelectionText(selectionRange); selector.show(entity => { const selectedText = selectionText || entity.name; @@ -96,7 +102,7 @@ export class Actions { try { const resp = await window.$http.post('/images/drawio', data); - this.#insertDrawing(resp.data, selectionRange); + this.#insertDrawing(resp.data as ImageManagerImage, selectionRange); DrawIO.close(); } catch (err) { this.handleDrawingUploadError(err); @@ -105,20 +111,23 @@ export class Actions { }); } - #insertDrawing(image, originalSelectionRange) { + #insertDrawing(image: ImageManagerImage, originalSelectionRange: SelectionRange) { const newText = `
`; this.#replaceSelection(newText, newText.length, originalSelectionRange); } // Show draw.io if enabled and handle save. - editDrawing(imgContainer) { + editDrawing(imgContainer: HTMLElement) { const {drawioUrl} = this.editor.config; if (!drawioUrl) { return; } const selectionRange = this.#getSelectionRange(); - const drawingId = imgContainer.getAttribute('drawio-diagram'); + const drawingId = imgContainer.getAttribute('drawio-diagram') || ''; + if (!drawingId) { + return; + } DrawIO.show(drawioUrl, () => DrawIO.load(drawingId), async pngData => { const data = { @@ -128,7 +137,8 @@ export class Actions { try { const resp = await window.$http.post('/images/drawio', data); - const newText = `
`; + const image = resp.data as ImageManagerImage; + const newText = `
`; const newContent = this.#getText().split('\n').map(line => { if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) { return newText; @@ -144,7 +154,7 @@ export class Actions { }); } - handleDrawingUploadError(error) { + handleDrawingUploadError(error: any): void { if (error.status === 413) { window.$events.emit('error', this.editor.config.text.serverUploadLimit); } else { @@ -162,7 +172,7 @@ export class Actions { } // Scroll to a specified text - scrollToText(searchText) { + scrollToText(searchText: string): void { if (!searchText) { return; } @@ -195,17 +205,15 @@ export class Actions { /** * Insert content into the editor. - * @param {String} content */ - insertContent(content) { + insertContent(content: string) { this.#replaceSelection(content, content.length); } /** * Prepend content to the editor. - * @param {String} content */ - prependContent(content) { + prependContent(content: string): void { content = this.#cleanTextForEditor(content); const selectionRange = this.#getSelectionRange(); const selectFrom = selectionRange.from + content.length + 1; @@ -215,19 +223,18 @@ export class Actions { /** * Append content to the editor. - * @param {String} content */ - appendContent(content) { + appendContent(content: string): void { content = this.#cleanTextForEditor(content); - this.#dispatchChange(this.editor.cm.state.doc.length, `\n${content}`); + const end = this.editor.cm.state.doc.length; + this.#dispatchChange(end, end, `\n${content}`); this.focus(); } /** * Replace the editor's contents - * @param {String} content */ - replaceContent(content) { + replaceContent(content: string): void { this.#setText(content); } @@ -235,7 +242,7 @@ export class Actions { * Replace the start of the line * @param {String} newStart */ - replaceLineStart(newStart) { + replaceLineStart(newStart: string): void { const selectionRange = this.#getSelectionRange(); const line = this.editor.cm.state.doc.lineAt(selectionRange.from); @@ -264,10 +271,8 @@ export class Actions { /** * Wrap the selection in the given contents start and end contents. - * @param {String} start - * @param {String} end */ - wrapSelection(start, end) { + wrapSelection(start: string, end: string): void { const selectRange = this.#getSelectionRange(); const selectionText = this.#getSelectionText(selectRange); if (!selectionText) { @@ -321,7 +326,7 @@ export class Actions { const formats = ['info', 'success', 'warning', 'danger']; const joint = formats.join('|'); const regex = new RegExp(`class="((${joint})\\s+callout|callout\\s+(${joint}))"`, 'i'); - const matches = regex.exec(line.text); + const matches = regex.exec(line.text) || ['']; const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase(); if (format === formats[formats.length - 1]) { @@ -343,9 +348,9 @@ export class Actions { } } - syncDisplayPosition(event) { + syncDisplayPosition(event: Event): void { // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html - const scrollEl = event.target; + const scrollEl = event.target as HTMLElement; const atEnd = Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 1; if (atEnd) { this.editor.display.scrollToIndex(-1); @@ -363,25 +368,19 @@ export class Actions { /** * Fetch and insert the template of the given ID. * The page-relative position provided can be used to determine insert location if possible. - * @param {String} templateId - * @param {Number} posX - * @param {Number} posY */ - async insertTemplate(templateId, posX, posY) { + async insertTemplate(templateId: string, posX: number, posY: number): Promise { const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); - const {data} = await window.$http.get(`/templates/${templateId}`); - const content = data.markdown || data.html; + const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string}; + const content = responseData.markdown || responseData.html; this.#dispatchChange(cursorPos, cursorPos, content, cursorPos); } /** * Insert multiple images from the clipboard from an event at the provided * screen coordinates (Typically form a paste event). - * @param {File[]} images - * @param {Number} posX - * @param {Number} posY */ - insertClipboardImages(images, posX, posY) { + insertClipboardImages(images: File[], posX: number, posY: number): void { const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); for (const image of images) { this.uploadImage(image, cursorPos); @@ -390,10 +389,8 @@ export class Actions { /** * Handle image upload and add image into markdown content - * @param {File} file - * @param {?Number} position */ - async uploadImage(file, position = null) { + async uploadImage(file: File, position: number|null = null): Promise { if (file === null || file.type.indexOf('image') !== 0) return; let ext = 'png'; @@ -403,7 +400,9 @@ export class Actions { if (file.name) { const fileNameMatches = file.name.match(/\.(.+)$/); - if (fileNameMatches.length > 1) ext = fileNameMatches[1]; + if (fileNameMatches && fileNameMatches.length > 1) { + ext = fileNameMatches[1]; + } } // Insert image into markdown @@ -418,10 +417,10 @@ export class Actions { formData.append('uploaded_to', this.editor.config.pageId); try { - const {data} = await window.$http.post('/images/gallery', formData); - const newContent = `[![](${data.thumbs.display})](${data.url})`; + const image = (await window.$http.post('/images/gallery', formData)).data as ImageManagerImage; + const newContent = `[![](${image.thumbs.display})](${image.url})`; this.#findAndReplaceContent(placeHolderText, newContent); - } catch (err) { + } catch (err: any) { window.$events.error(err?.data?.message || this.editor.config.text.imageUploadError); this.#findAndReplaceContent(placeHolderText, ''); console.error(err); @@ -438,10 +437,8 @@ export class Actions { /** * Set the text of the current editor instance. - * @param {String} text - * @param {?SelectionRange} selectionRange */ - #setText(text, selectionRange = null) { + #setText(text: string, selectionRange: SelectionRange|null = null) { selectionRange = selectionRange || this.#getSelectionRange(); const newDoc = this.editor.cm.state.toText(text); const newSelectFrom = Math.min(selectionRange.from, newDoc.length); @@ -457,12 +454,9 @@ export class Actions { * Replace the current selection and focus the editor. * Takes an offset for the cursor, after the change, relative to the start of the provided string. * Can be provided a selection range to use instead of the current selection range. - * @param {String} newContent - * @param {Number} cursorOffset - * @param {?SelectionRange} selectionRange */ - #replaceSelection(newContent, cursorOffset = 0, selectionRange = null) { - selectionRange = selectionRange || this.editor.cm.state.selection.main; + #replaceSelection(newContent: string, cursorOffset: number = 0, selectionRange: SelectionRange|null = null) { + selectionRange = selectionRange || this.#getSelectionRange(); const selectFrom = selectionRange.from + cursorOffset; this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom); this.focus(); @@ -470,48 +464,39 @@ export class Actions { /** * Get the text content of the main current selection. - * @param {SelectionRange} selectionRange - * @return {string} */ - #getSelectionText(selectionRange = null) { + #getSelectionText(selectionRange: SelectionRange|null = null): string { selectionRange = selectionRange || this.#getSelectionRange(); return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to); } /** * Get the range of the current main selection. - * @return {SelectionRange} */ - #getSelectionRange() { + #getSelectionRange(): SelectionRange { return this.editor.cm.state.selection.main; } /** * Cleans the given text to work with the editor. * Standardises line endings to what's expected. - * @param {String} text - * @return {String} */ - #cleanTextForEditor(text) { + #cleanTextForEditor(text: string): string { return text.replace(/\r\n|\r/g, '\n'); } /** * Find and replace the first occurrence of [search] with [replace] - * @param {String} search - * @param {String} replace */ - #findAndReplaceContent(search, replace) { + #findAndReplaceContent(search: string, replace: string): void { const newText = this.#getText().replace(search, replace); this.#setText(newText); } /** * Wrap the line in the given start and end contents. - * @param {String} start - * @param {String} end */ - #wrapLine(start, end) { + #wrapLine(start: string, end: string): void { const selectionRange = this.#getSelectionRange(); const line = this.editor.cm.state.doc.lineAt(selectionRange.from); const lineContent = line.text; @@ -531,14 +516,16 @@ export class Actions { /** * Dispatch changes to the editor. - * @param {Number} from - * @param {?Number} to - * @param {?String} text - * @param {?Number} selectFrom - * @param {?Number} selectTo */ - #dispatchChange(from, to = null, text = null, selectFrom = null, selectTo = null) { - const tr = {changes: {from, to, insert: text}}; + #dispatchChange(from: number, to: number|null = null, text: string|null = null, selectFrom: number|null = null, selectTo: number|null = null): void { + const change: ChangeSpec = {from}; + if (to) { + change.to = to; + } + if (text) { + change.insert = text; + } + const tr: TransactionSpec = {changes: change}; if (selectFrom) { tr.selection = {anchor: selectFrom}; @@ -557,7 +544,7 @@ export class Actions { * @param {Number} to * @param {Boolean} scrollIntoView */ - #setSelection(from, to, scrollIntoView = false) { + #setSelection(from: number, to: number, scrollIntoView = false) { this.editor.cm.dispatch({ selection: {anchor: from, head: to}, scrollIntoView, diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index 664767605b8..61b2e84573d 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -5,7 +5,7 @@ import {Clipboard} from '../services/clipboard.ts'; /** * Initiate the codemirror instance for the markdown editor. * @param {MarkdownEditor} editor - * @returns {Promise} + * @returns {Promise} */ export async function init(editor) { const Code = await window.importVersioned('code'); diff --git a/resources/js/markdown/common-events.js b/resources/js/markdown/common-events.js deleted file mode 100644 index c3d803f7048..00000000000 --- a/resources/js/markdown/common-events.js +++ /dev/null @@ -1,32 +0,0 @@ -function getContentToInsert({html, markdown}) { - return markdown || html; -} - -/** - * @param {MarkdownEditor} editor - */ -export function listen(editor) { - window.$events.listen('editor::replace', eventContent => { - const markdown = getContentToInsert(eventContent); - editor.actions.replaceContent(markdown); - }); - - window.$events.listen('editor::append', eventContent => { - const markdown = getContentToInsert(eventContent); - editor.actions.appendContent(markdown); - }); - - window.$events.listen('editor::prepend', eventContent => { - const markdown = getContentToInsert(eventContent); - editor.actions.prependContent(markdown); - }); - - window.$events.listen('editor::insert', eventContent => { - const markdown = getContentToInsert(eventContent); - editor.actions.insertContent(markdown); - }); - - window.$events.listen('editor::focus', () => { - editor.actions.focus(); - }); -} diff --git a/resources/js/markdown/common-events.ts b/resources/js/markdown/common-events.ts new file mode 100644 index 00000000000..4bfc4bb4619 --- /dev/null +++ b/resources/js/markdown/common-events.ts @@ -0,0 +1,36 @@ +import {MarkdownEditor} from "./index.mjs"; + +export interface HtmlOrMarkdown { + html: string; + markdown: string; +} + +function getContentToInsert({html, markdown}: {html: string, markdown: string}): string { + return markdown || html; +} + +export function listenToCommonEvents(editor: MarkdownEditor): void { + window.$events.listen('editor::replace', (eventContent: HtmlOrMarkdown) => { + const markdown = getContentToInsert(eventContent); + editor.actions.replaceContent(markdown); + }); + + window.$events.listen('editor::append', (eventContent: HtmlOrMarkdown) => { + const markdown = getContentToInsert(eventContent); + editor.actions.appendContent(markdown); + }); + + window.$events.listen('editor::prepend', (eventContent: HtmlOrMarkdown) => { + const markdown = getContentToInsert(eventContent); + editor.actions.prependContent(markdown); + }); + + window.$events.listen('editor::insert', (eventContent: HtmlOrMarkdown) => { + const markdown = getContentToInsert(eventContent); + editor.actions.insertContent(markdown); + }); + + window.$events.listen('editor::focus', () => { + editor.actions.focus(); + }); +} diff --git a/resources/js/markdown/index.mjs b/resources/js/markdown/index.mjs deleted file mode 100644 index 46c35c850ac..00000000000 --- a/resources/js/markdown/index.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import {Markdown} from './markdown'; -import {Display} from './display'; -import {Actions} from './actions'; -import {Settings} from './settings'; -import {listen} from './common-events'; -import {init as initCodemirror} from './codemirror'; - -/** - * Initiate a new markdown editor instance. - * @param {MarkdownEditorConfig} config - * @returns {Promise} - */ -export async function init(config) { - /** - * @type {MarkdownEditor} - */ - const editor = { - config, - markdown: new Markdown(), - settings: new Settings(config.settingInputs), - }; - - editor.actions = new Actions(editor); - editor.display = new Display(editor); - editor.cm = await initCodemirror(editor); - - listen(editor); - - return editor; -} - -/** - * @typedef MarkdownEditorConfig - * @property {String} pageId - * @property {Element} container - * @property {Element} displayEl - * @property {HTMLTextAreaElement} inputEl - * @property {String} drawioUrl - * @property {HTMLInputElement[]} settingInputs - * @property {Object} text - */ - -/** - * @typedef MarkdownEditor - * @property {MarkdownEditorConfig} config - * @property {Display} display - * @property {Markdown} markdown - * @property {Actions} actions - * @property {EditorView} cm - * @property {Settings} settings - */ diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts new file mode 100644 index 00000000000..46345ccfd6f --- /dev/null +++ b/resources/js/markdown/index.mts @@ -0,0 +1,52 @@ +import {Markdown} from './markdown'; +import {Display} from './display'; +import {Actions} from './actions'; +import {Settings} from './settings'; +import {listenToCommonEvents} from './common-events'; +import {init as initCodemirror} from './codemirror'; +import {EditorView} from "@codemirror/view"; + +export interface MarkdownEditorConfig { + pageId: string; + container: Element; + displayEl: Element; + inputEl: HTMLTextAreaElement; + drawioUrl: string; + settingInputs: HTMLInputElement[]; + text: Record; +} + +export interface MarkdownEditor { + config: MarkdownEditorConfig; + display: Display; + markdown: Markdown; + actions: Actions; + cm: EditorView; + settings: Settings; +} + +/** + * Initiate a new Markdown editor instance. + * @param {MarkdownEditorConfig} config + * @returns {Promise} + */ +export async function init(config) { + /** + * @type {MarkdownEditor} + */ + const editor: MarkdownEditor = { + config, + markdown: new Markdown(), + settings: new Settings(config.settingInputs), + }; + + editor.actions = new Actions(editor); + editor.display = new Display(editor); + editor.cm = await initCodemirror(editor); + + listenToCommonEvents(editor); + + return editor; +} + + diff --git a/resources/js/markdown/settings.js b/resources/js/markdown/settings.js index b843aaa8a2b..e2e1fce5e37 100644 --- a/resources/js/markdown/settings.js +++ b/resources/js/markdown/settings.js @@ -5,6 +5,7 @@ export class Settings { scrollSync: true, showPreview: true, editorWidth: 50, + plainEditor: false, }; this.changeListeners = {}; this.loadFromLocalStorage(); diff --git a/resources/views/pages/parts/markdown-editor.blade.php b/resources/views/pages/parts/markdown-editor.blade.php index ac62443f985..5b1761b7656 100644 --- a/resources/views/pages/parts/markdown-editor.blade.php +++ b/resources/views/pages/parts/markdown-editor.blade.php @@ -26,6 +26,10 @@ class="flex-fill flex code-fill">
@include('form.custom-checkbox', ['name' => 'md-scrollSync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => true, 'checked' => true])
+
+
+ @include('form.custom-checkbox', ['name' => 'md-plainEditor', 'label' => trans('entities.pages_md_plain_editor'), 'value' => true, 'checked' => false]) +
From 61adc735c8d525907dbb8b1b554c24d303cec229 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 20 Jul 2025 15:05:19 +0100 Subject: [PATCH 02/11] MD Editor: Finished conversion to Typescript --- package-lock.json | 26 ++++++ package.json | 1 + .../markdown/{codemirror.js => codemirror.ts} | 38 +++++---- .../js/markdown/{display.js => display.ts} | 78 ++++++++++-------- resources/js/markdown/index.mts | 11 +-- .../js/markdown/{markdown.js => markdown.ts} | 11 ++- resources/js/markdown/settings.js | 64 --------------- resources/js/markdown/settings.ts | 82 +++++++++++++++++++ .../markdown/{shortcuts.js => shortcuts.ts} | 15 ++-- 9 files changed, 188 insertions(+), 138 deletions(-) rename resources/js/markdown/{codemirror.js => codemirror.ts} (68%) rename resources/js/markdown/{display.js => display.ts} (52%) rename resources/js/markdown/{markdown.js => markdown.ts} (68%) delete mode 100644 resources/js/markdown/settings.js create mode 100644 resources/js/markdown/settings.ts rename resources/js/markdown/{shortcuts.js => shortcuts.ts} (85%) diff --git a/package-lock.json b/package-lock.json index 926a6d9e3f5..0348fd1ed42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "devDependencies": { "@eslint/js": "^9.21.0", "@lezer/generator": "^1.7.2", + "@types/markdown-it": "^14.1.2", "@types/sortablejs": "^1.15.8", "chokidar-cli": "^3.0", "esbuild": "^0.25.0", @@ -2508,6 +2509,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.15.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", diff --git a/package.json b/package.json index 5d94537d140..151338d8c6e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "devDependencies": { "@eslint/js": "^9.21.0", "@lezer/generator": "^1.7.2", + "@types/markdown-it": "^14.1.2", "@types/sortablejs": "^1.15.8", "chokidar-cli": "^3.0", "esbuild": "^0.25.0", diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.ts similarity index 68% rename from resources/js/markdown/codemirror.js rename to resources/js/markdown/codemirror.ts index 61b2e84573d..a3b81418f9f 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.ts @@ -1,19 +1,16 @@ import {provideKeyBindings} from './shortcuts'; -import {debounce} from '../services/util.ts'; -import {Clipboard} from '../services/clipboard.ts'; +import {debounce} from '../services/util'; +import {Clipboard} from '../services/clipboard'; +import {EditorView, ViewUpdate} from "@codemirror/view"; +import {MarkdownEditor} from "./index.mjs"; /** * Initiate the codemirror instance for the markdown editor. - * @param {MarkdownEditor} editor - * @returns {Promise} */ -export async function init(editor) { - const Code = await window.importVersioned('code'); +export async function init(editor: MarkdownEditor): Promise { + const Code = await window.importVersioned('code') as (typeof import('../code/index.mjs')); - /** - * @param {ViewUpdate} v - */ - function onViewUpdate(v) { + function onViewUpdate(v: ViewUpdate) { if (v.docChanged) { editor.actions.updateAndRender(); } @@ -27,9 +24,13 @@ export async function init(editor) { const domEventHandlers = { // Handle scroll to sync display view - scroll: event => syncActive && onScrollDebounced(event), + scroll: (event: Event) => syncActive && onScrollDebounced(event), // Handle image & content drag n drop - drop: event => { + drop: (event: DragEvent) => { + if (!event.dataTransfer) { + return; + } + const templateId = event.dataTransfer.getData('bookstack/template'); if (templateId) { event.preventDefault(); @@ -45,12 +46,16 @@ export async function init(editor) { } }, // Handle dragover event to allow as drop-target in chrome - dragover: event => { + dragover: (event: DragEvent) => { event.preventDefault(); }, // Handle image paste - paste: event => { - const clipboard = new Clipboard(event.clipboardData || event.dataTransfer); + paste: (event: ClipboardEvent) => { + if (!event.clipboardData) { + return; + } + + const clipboard = new Clipboard(event.clipboardData); // Don't handle the event ourselves if no items exist of contains table-looking data if (!clipboard.hasItems() || clipboard.containsTabularData()) { @@ -71,8 +76,9 @@ export async function init(editor) { provideKeyBindings(editor), ); - // Add editor view to window for easy access/debugging. + // Add editor view to the window for easy access/debugging. // Not part of official API/Docs + // @ts-ignore window.mdEditorView = cm; return cm; diff --git a/resources/js/markdown/display.js b/resources/js/markdown/display.ts similarity index 52% rename from resources/js/markdown/display.js rename to resources/js/markdown/display.ts index 60be26b5fb7..3eb7e5c6aa2 100644 --- a/resources/js/markdown/display.js +++ b/resources/js/markdown/display.ts @@ -1,35 +1,36 @@ -import {patchDomFromHtmlString} from '../services/vdom.ts'; +import { patchDomFromHtmlString } from '../services/vdom'; +import {MarkdownEditor} from "./index.mjs"; export class Display { + protected editor: MarkdownEditor; + protected container: HTMLIFrameElement; + protected doc: Document | null = null; + protected lastDisplayClick: number = 0; - /** - * @param {MarkdownEditor} editor - */ - constructor(editor) { + constructor(editor: MarkdownEditor) { this.editor = editor; this.container = editor.config.displayEl; - this.doc = null; - this.lastDisplayClick = 0; - - if (this.container.contentDocument.readyState === 'complete') { + if (this.container.contentDocument?.readyState === 'complete') { this.onLoad(); } else { this.container.addEventListener('load', this.onLoad.bind(this)); } - this.updateVisibility(editor.settings.get('showPreview')); - editor.settings.onChange('showPreview', show => this.updateVisibility(show)); + this.updateVisibility(Boolean(editor.settings.get('showPreview'))); + editor.settings.onChange('showPreview', (show) => this.updateVisibility(Boolean(show))); } - updateVisibility(show) { - const wrap = this.container.closest('.markdown-editor-wrap'); - wrap.style.display = show ? null : 'none'; + protected updateVisibility(show: boolean): void { + const wrap = this.container.closest('.markdown-editor-wrap') as HTMLElement; + wrap.style.display = show ? '' : 'none'; } - onLoad() { + protected onLoad(): void { this.doc = this.container.contentDocument; + if (!this.doc) return; + this.loadStylesIntoDisplay(); this.doc.body.className = 'page-content'; @@ -37,20 +38,20 @@ export class Display { this.doc.addEventListener('click', this.onDisplayClick.bind(this)); } - /** - * @param {MouseEvent} event - */ - onDisplayClick(event) { + protected onDisplayClick(event: MouseEvent): void { const isDblClick = Date.now() - this.lastDisplayClick < 300; - const link = event.target.closest('a'); + const link = (event.target as Element).closest('a'); if (link !== null) { event.preventDefault(); - window.open(link.getAttribute('href')); + const href = link.getAttribute('href'); + if (href) { + window.open(href); + } return; } - const drawing = event.target.closest('[drawio-diagram]'); + const drawing = (event.target as Element).closest('[drawio-diagram]') as HTMLElement; if (drawing !== null && isDblClick) { this.editor.actions.editDrawing(drawing); return; @@ -59,10 +60,12 @@ export class Display { this.lastDisplayClick = Date.now(); } - loadStylesIntoDisplay() { + protected loadStylesIntoDisplay(): void { + if (!this.doc) return; + this.doc.documentElement.classList.add('markdown-editor-display'); - // Set display to be dark mode if parent is + // Set display to be dark mode if the parent is if (document.documentElement.classList.contains('dark-mode')) { this.doc.documentElement.style.backgroundColor = '#222'; this.doc.documentElement.classList.add('dark-mode'); @@ -71,24 +74,25 @@ export class Display { this.doc.head.innerHTML = ''; const styles = document.head.querySelectorAll('style,link[rel=stylesheet]'); for (const style of styles) { - const copy = style.cloneNode(true); + const copy = style.cloneNode(true) as HTMLElement; this.doc.head.appendChild(copy); } } /** * Patch the display DOM with the given HTML content. - * @param {String} html */ - patchWithHtml(html) { - const {body} = this.doc; + public patchWithHtml(html: string): void { + if (!this.doc) return; + + const { body } = this.doc; if (body.children.length === 0) { const wrap = document.createElement('div'); this.doc.body.append(wrap); } - const target = body.children[0]; + const target = body.children[0] as HTMLElement; patchDomFromHtmlString(target, html); } @@ -96,14 +100,16 @@ export class Display { /** * Scroll to the given block index within the display content. * Will scroll to the end if the index is -1. - * @param {Number} index */ - scrollToIndex(index) { - const elems = this.doc.body?.children[0]?.children; - if (elems && elems.length <= index) return; + public scrollToIndex(index: number): void { + const elems = this.doc?.body?.children[0]?.children; + if (!elems || elems.length <= index) return; const topElem = (index === -1) ? elems[elems.length - 1] : elems[index]; - topElem.scrollIntoView({block: 'start', inline: 'nearest', behavior: 'smooth'}); + (topElem as Element).scrollIntoView({ + block: 'start', + inline: 'nearest', + behavior: 'smooth' + }); } - -} +} \ No newline at end of file diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index 46345ccfd6f..d487b7972e8 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -9,7 +9,7 @@ import {EditorView} from "@codemirror/view"; export interface MarkdownEditorConfig { pageId: string; container: Element; - displayEl: Element; + displayEl: HTMLIFrameElement; inputEl: HTMLTextAreaElement; drawioUrl: string; settingInputs: HTMLInputElement[]; @@ -27,18 +27,13 @@ export interface MarkdownEditor { /** * Initiate a new Markdown editor instance. - * @param {MarkdownEditorConfig} config - * @returns {Promise} */ -export async function init(config) { - /** - * @type {MarkdownEditor} - */ +export async function init(config: MarkdownEditorConfig): Promise { const editor: MarkdownEditor = { config, markdown: new Markdown(), settings: new Settings(config.settingInputs), - }; + } as MarkdownEditor; editor.actions = new Actions(editor); editor.display = new Display(editor); diff --git a/resources/js/markdown/markdown.js b/resources/js/markdown/markdown.ts similarity index 68% rename from resources/js/markdown/markdown.js rename to resources/js/markdown/markdown.ts index e63184accaf..07ea09e9113 100644 --- a/resources/js/markdown/markdown.js +++ b/resources/js/markdown/markdown.ts @@ -1,7 +1,9 @@ import MarkdownIt from 'markdown-it'; +// @ts-ignore import mdTasksLists from 'markdown-it-task-lists'; export class Markdown { + protected renderer: MarkdownIt; constructor() { this.renderer = new MarkdownIt({html: true}); @@ -9,19 +11,16 @@ export class Markdown { } /** - * Get the front-end render used to convert markdown to HTML. - * @returns {MarkdownIt} + * Get the front-end render used to convert Markdown to HTML. */ - getRenderer() { + getRenderer(): MarkdownIt { return this.renderer; } /** * Convert the given Markdown to HTML. - * @param {String} markdown - * @returns {String} */ - render(markdown) { + render(markdown: string): string { return this.renderer.render(markdown); } diff --git a/resources/js/markdown/settings.js b/resources/js/markdown/settings.js deleted file mode 100644 index e2e1fce5e37..00000000000 --- a/resources/js/markdown/settings.js +++ /dev/null @@ -1,64 +0,0 @@ -export class Settings { - - constructor(settingInputs) { - this.settingMap = { - scrollSync: true, - showPreview: true, - editorWidth: 50, - plainEditor: false, - }; - this.changeListeners = {}; - this.loadFromLocalStorage(); - this.applyToInputs(settingInputs); - this.listenToInputChanges(settingInputs); - } - - applyToInputs(inputs) { - for (const input of inputs) { - const name = input.getAttribute('name').replace('md-', ''); - input.checked = this.settingMap[name]; - } - } - - listenToInputChanges(inputs) { - for (const input of inputs) { - input.addEventListener('change', () => { - const name = input.getAttribute('name').replace('md-', ''); - this.set(name, input.checked); - }); - } - } - - loadFromLocalStorage() { - const lsValString = window.localStorage.getItem('md-editor-settings'); - if (!lsValString) { - return; - } - - const lsVals = JSON.parse(lsValString); - for (const [key, value] of Object.entries(lsVals)) { - if (value !== null && this.settingMap[key] !== undefined) { - this.settingMap[key] = value; - } - } - } - - set(key, value) { - this.settingMap[key] = value; - window.localStorage.setItem('md-editor-settings', JSON.stringify(this.settingMap)); - for (const listener of (this.changeListeners[key] || [])) { - listener(value); - } - } - - get(key) { - return this.settingMap[key] || null; - } - - onChange(key, callback) { - const listeners = this.changeListeners[key] || []; - listeners.push(callback); - this.changeListeners[key] = listeners; - } - -} diff --git a/resources/js/markdown/settings.ts b/resources/js/markdown/settings.ts new file mode 100644 index 00000000000..c446cbe052e --- /dev/null +++ b/resources/js/markdown/settings.ts @@ -0,0 +1,82 @@ +type ChangeListener = (value: boolean|number) => void; + +export class Settings { + protected changeListeners: Record = {}; + + protected settingMap: Record = { + scrollSync: true, + showPreview: true, + editorWidth: 50, + plainEditor: false, + }; + + constructor(settingInputs: HTMLInputElement[]) { + this.loadFromLocalStorage(); + this.applyToInputs(settingInputs); + this.listenToInputChanges(settingInputs); + } + + protected applyToInputs(inputs: HTMLInputElement[]): void { + for (const input of inputs) { + const name = input.getAttribute('name')?.replace('md-', ''); + if (name && name in this.settingMap) { + const value = this.settingMap[name]; + if (typeof value === 'boolean') { + input.checked = value; + } else { + input.value = value.toString(); + } + } + } + } + + protected listenToInputChanges(inputs: HTMLInputElement[]): void { + for (const input of inputs) { + input.addEventListener('change', () => { + const name = input.getAttribute('name')?.replace('md-', ''); + if (name && name in this.settingMap) { + let value = (input.type === 'checkbox') ? input.checked : Number(input.value); + this.set(name, value); + } + }); + } + } + + protected loadFromLocalStorage(): void { + const lsValString = window.localStorage.getItem('md-editor-settings'); + if (!lsValString) { + return; + } + + try { + const lsVals = JSON.parse(lsValString); + for (const [key, value] of Object.entries(lsVals)) { + if (value !== null && value !== undefined && key in this.settingMap) { + this.settingMap[key] = value as boolean|number; + } + } + } catch (error) { + console.warn('Failed to parse settings from localStorage:', error); + } + } + + public set(key: string, value: boolean|number): void { + this.settingMap[key] = value; + window.localStorage.setItem('md-editor-settings', JSON.stringify(this.settingMap)); + + const listeners = this.changeListeners[key] || []; + for (const listener of listeners) { + listener(value); + } + } + + public get(key: string): number|boolean|null { + return this.settingMap[key] ?? null; + } + + public onChange(key: string, callback: ChangeListener): void { + const listeners = this.changeListeners[key] || []; + listeners.push(callback); + this.changeListeners[key] = listeners; + } +} \ No newline at end of file diff --git a/resources/js/markdown/shortcuts.js b/resources/js/markdown/shortcuts.ts similarity index 85% rename from resources/js/markdown/shortcuts.js rename to resources/js/markdown/shortcuts.ts index 543e6dcdde6..c746b52e703 100644 --- a/resources/js/markdown/shortcuts.js +++ b/resources/js/markdown/shortcuts.ts @@ -1,10 +1,11 @@ +import {MarkdownEditor} from "./index.mjs"; +import {KeyBinding} from "@codemirror/view"; + /** * Provide shortcuts for the editor instance. - * @param {MarkdownEditor} editor - * @returns {Object} */ -function provide(editor) { - const shortcuts = {}; +function provide(editor: MarkdownEditor): Record void> { + const shortcuts: Record void> = {}; // Insert Image shortcut shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage(); @@ -42,14 +43,12 @@ function provide(editor) { /** * Get the editor shortcuts in CodeMirror keybinding format. - * @param {MarkdownEditor} editor - * @return {{key: String, run: function, preventDefault: boolean}[]} */ -export function provideKeyBindings(editor) { +export function provideKeyBindings(editor: MarkdownEditor): KeyBinding[] { const shortcuts = provide(editor); const keyBindings = []; - const wrapAction = action => () => { + const wrapAction = (action: ()=>void) => () => { action(); return true; }; From ec07793cda66d319bf716ef3dcbbe2fe19f0c355 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 21 Jul 2025 11:49:58 +0100 Subject: [PATCH 03/11] MD Editor: Started work on input interface Created implementation for codemirror, yet to use it. --- resources/js/global.d.ts | 4 +- resources/js/markdown/codemirror.ts | 7 +- resources/js/markdown/index.mts | 6 +- resources/js/markdown/inputs/codemirror.ts | 120 +++++++++++++++++++++ resources/js/markdown/inputs/interface.ts | 71 ++++++++++++ 5 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 resources/js/markdown/inputs/codemirror.ts create mode 100644 resources/js/markdown/inputs/interface.ts diff --git a/resources/js/global.d.ts b/resources/js/global.d.ts index b637c97c1b9..239f4b9249a 100644 --- a/resources/js/global.d.ts +++ b/resources/js/global.d.ts @@ -15,4 +15,6 @@ declare global { baseUrl: (path: string) => string; importVersioned: (module: string) => Promise; } -} \ No newline at end of file +} + +export type CodeModule = (typeof import('./code/index.mjs')); \ No newline at end of file diff --git a/resources/js/markdown/codemirror.ts b/resources/js/markdown/codemirror.ts index a3b81418f9f..1b54c58196b 100644 --- a/resources/js/markdown/codemirror.ts +++ b/resources/js/markdown/codemirror.ts @@ -3,13 +3,12 @@ import {debounce} from '../services/util'; import {Clipboard} from '../services/clipboard'; import {EditorView, ViewUpdate} from "@codemirror/view"; import {MarkdownEditor} from "./index.mjs"; +import {CodeModule} from "../global"; /** - * Initiate the codemirror instance for the markdown editor. + * Initiate the codemirror instance for the MarkDown editor. */ -export async function init(editor: MarkdownEditor): Promise { - const Code = await window.importVersioned('code') as (typeof import('../code/index.mjs')); - +export function init(editor: MarkdownEditor, Code: CodeModule): EditorView { function onViewUpdate(v: ViewUpdate) { if (v.docChanged) { editor.actions.updateAndRender(); diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index d487b7972e8..b983285d907 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -5,6 +5,8 @@ import {Settings} from './settings'; import {listenToCommonEvents} from './common-events'; import {init as initCodemirror} from './codemirror'; import {EditorView} from "@codemirror/view"; +import {importVersioned} from "../services/util"; +import {CodeModule} from "../global"; export interface MarkdownEditorConfig { pageId: string; @@ -29,6 +31,8 @@ export interface MarkdownEditor { * Initiate a new Markdown editor instance. */ export async function init(config: MarkdownEditorConfig): Promise { + const Code = await window.importVersioned('code') as CodeModule; + const editor: MarkdownEditor = { config, markdown: new Markdown(), @@ -37,7 +41,7 @@ export async function init(config: MarkdownEditorConfig): Promise { + this.editor.cm.scrollDOM.scrollTop = scrollTop; + }); + } + + spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection | null = null) { + const end = (selection?.from === selection?.to) ? null : selection?.to; + this.dispatchChange(from, to, newText, selection?.from, end) + } + + appendText(text: string) { + const end = this.editor.cm.state.doc.length; + this.dispatchChange(end, end, `\n${text}`); + } + + getLineText(lineIndex: number = -1): string { + const index = lineIndex > -1 ? lineIndex : this.getSelection().from; + return this.editor.cm.state.doc.lineAt(index).text; + } + + wrapLine(start: string, end: string) { + const selectionRange = this.getSelection(); + const line = this.editor.cm.state.doc.lineAt(selectionRange.from); + const lineContent = line.text; + let newLineContent; + let lineOffset = 0; + + if (lineContent.startsWith(start) && lineContent.endsWith(end)) { + newLineContent = lineContent.slice(start.length, lineContent.length - end.length); + lineOffset = -(start.length); + } else { + newLineContent = `${start}${lineContent}${end}`; + lineOffset = start.length; + } + + this.dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset); + } + + coordsToSelection(x: number, y: number): MarkdownEditorInputSelection { + const cursorPos = this.editor.cm.posAtCoords({x, y}, false); + return {from: cursorPos, to: cursorPos}; + } + + /** + * Dispatch changes to the editor. + */ + protected dispatchChange(from: number, to: number|null = null, text: string|null = null, selectFrom: number|null = null, selectTo: number|null = null): void { + const change: ChangeSpec = {from}; + if (to) { + change.to = to; + } + if (text) { + change.insert = text; + } + const tr: TransactionSpec = {changes: change}; + + if (selectFrom) { + tr.selection = {anchor: selectFrom}; + if (selectTo) { + tr.selection.head = selectTo; + } + } + + this.cm.dispatch(tr); + } + +} \ No newline at end of file diff --git a/resources/js/markdown/inputs/interface.ts b/resources/js/markdown/inputs/interface.ts new file mode 100644 index 00000000000..aafd86f9187 --- /dev/null +++ b/resources/js/markdown/inputs/interface.ts @@ -0,0 +1,71 @@ + +export interface MarkdownEditorInputSelection { + from: number; + to: number; +} + +export interface MarkdownEditorInput { + /** + * Focus on the editor. + */ + focus(): void; + + /** + * Get the current selection range. + */ + getSelection(): MarkdownEditorInputSelection; + + /** + * Get the text of the given (or current) selection range. + */ + getSelectionText(selection: MarkdownEditorInputSelection|null = null): string; + + /** + * Set the selection range of the editor. + */ + setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false): void; + + /** + * Get the full text of the input. + */ + getText(): string; + + /** + * Get just the text which is above (out) the current view range. + * This is used for position estimation. + */ + getTextAboveView(): string; + + /** + * Set the full text of the input. + * Optionally can provide a selection to restore after setting text. + */ + setText(text: string, selection: MarkdownEditorInputSelection|null = null): void; + + /** + * Splice in/out text within the input. + * Optionally can provide a selection to restore after setting text. + */ + spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection|null = null): void; + + /** + * Append text to the end of the editor. + */ + appendText(text: string): void; + + /** + * Get the text of the given line number otherwise the text + * of the current selected line. + */ + getLineText(lineIndex:number = -1): string; + + /** + * Wrap the current line in the given start/end contents. + */ + wrapLine(start: string, end: string): void; + + /** + * Convert the given screen coords to a selection position within the input. + */ + coordsToSelection(x: number, y: number): MarkdownEditorInputSelection; +} \ No newline at end of file From 5ffec2c52d6b6c1cf61e53813fe4bfbece3da1aa Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 21 Jul 2025 14:24:51 +0100 Subject: [PATCH 04/11] MD Editor: Updated actions to use input interface --- resources/js/markdown/actions.ts | 241 +++++++-------------- resources/js/markdown/index.mts | 10 +- resources/js/markdown/inputs/codemirror.ts | 82 +++---- resources/js/markdown/inputs/interface.ts | 19 +- 4 files changed, 136 insertions(+), 216 deletions(-) diff --git a/resources/js/markdown/actions.ts b/resources/js/markdown/actions.ts index c6210809cce..ed4ee5904ba 100644 --- a/resources/js/markdown/actions.ts +++ b/resources/js/markdown/actions.ts @@ -1,7 +1,7 @@ import * as DrawIO from '../services/drawio'; import {MarkdownEditor} from "./index.mjs"; import {EntitySelectorPopup, ImageManager} from "../components"; -import {ChangeSpec, SelectionRange, TransactionSpec} from "@codemirror/state"; +import {MarkdownEditorInputSelection} from "./inputs/interface"; interface ImageManagerImage { id: number; @@ -23,7 +23,7 @@ export class Actions { } updateAndRender() { - const content = this.#getText(); + const content = this.editor.input.getText(); this.editor.config.inputEl.value = content; const html = this.editor.markdown.render(content); @@ -43,26 +43,26 @@ export class Actions { imageManager.show((image: ImageManagerImage) => { const imageUrl = image.thumbs?.display || image.url; - const selectedText = this.#getSelectionText(); + const selectedText = this.editor.input.getSelectionText(); const newText = `[![${selectedText || image.name}](${imageUrl})](${image.url})`; this.#replaceSelection(newText, newText.length); }, 'gallery'); } insertImage() { - const newText = `![${this.#getSelectionText()}](http://)`; + const newText = `![${this.editor.input.getSelectionText()}](http://)`; this.#replaceSelection(newText, newText.length - 1); } insertLink() { - const selectedText = this.#getSelectionText(); + const selectedText = this.editor.input.getSelectionText(); const newText = `[${selectedText}]()`; const cursorPosDiff = (selectedText === '') ? -3 : -1; this.#replaceSelection(newText, newText.length + cursorPosDiff); } showImageManager() { - const selectionRange = this.#getSelectionRange(); + const selectionRange = this.editor.input.getSelection(); const imageManager = window.$components.first('image-manager') as ImageManager; imageManager.show((image: ImageManagerImage) => { this.#insertDrawing(image, selectionRange); @@ -71,10 +71,10 @@ export class Actions { // Show the popup link selector and insert a link when finished showLinkSelector() { - const selectionRange = this.#getSelectionRange(); + const selectionRange = this.editor.input.getSelection(); const selector = window.$components.first('entity-selector-popup') as EntitySelectorPopup; - const selectionText = this.#getSelectionText(selectionRange); + const selectionText = this.editor.input.getSelectionText(selectionRange); selector.show(entity => { const selectedText = selectionText || entity.name; const newText = `[${selectedText}](${entity.link})`; @@ -92,7 +92,7 @@ export class Actions { const url = this.editor.config.drawioUrl; if (!url) return; - const selectionRange = this.#getSelectionRange(); + const selectionRange = this.editor.input.getSelection(); DrawIO.show(url, () => Promise.resolve(''), async pngData => { const data = { @@ -111,7 +111,7 @@ export class Actions { }); } - #insertDrawing(image: ImageManagerImage, originalSelectionRange: SelectionRange) { + #insertDrawing(image: ImageManagerImage, originalSelectionRange: MarkdownEditorInputSelection) { const newText = `
`; this.#replaceSelection(newText, newText.length, originalSelectionRange); } @@ -123,7 +123,7 @@ export class Actions { return; } - const selectionRange = this.#getSelectionRange(); + const selectionRange = this.editor.input.getSelection(); const drawingId = imgContainer.getAttribute('drawio-diagram') || ''; if (!drawingId) { return; @@ -139,13 +139,13 @@ export class Actions { const resp = await window.$http.post('/images/drawio', data); const image = resp.data as ImageManagerImage; const newText = `
`; - const newContent = this.#getText().split('\n').map(line => { + const newContent = this.editor.input.getText().split('\n').map(line => { if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) { return newText; } return line; }).join('\n'); - this.#setText(newContent, selectionRange); + this.editor.input.setText(newContent, selectionRange); DrawIO.close(); } catch (err) { this.handleDrawingUploadError(err); @@ -177,30 +177,15 @@ export class Actions { return; } - const text = this.editor.cm.state.doc; - let lineCount = 1; - let scrollToLine = -1; - for (const line of text.iterLines()) { - if (line.includes(searchText)) { - scrollToLine = lineCount; - break; - } - lineCount += 1; + const lineRange = this.editor.input.searchForLineContaining(searchText); + if (lineRange) { + this.editor.input.setSelection(lineRange, true); + this.editor.input.focus(); } - - if (scrollToLine === -1) { - return; - } - - const line = text.line(scrollToLine); - this.#setSelection(line.from, line.to, true); - this.focus(); } focus() { - if (!this.editor.cm.hasFocus) { - this.editor.cm.focus(); - } + this.editor.input.focus(); } /** @@ -215,10 +200,10 @@ export class Actions { */ prependContent(content: string): void { content = this.#cleanTextForEditor(content); - const selectionRange = this.#getSelectionRange(); + const selectionRange = this.editor.input.getSelection(); const selectFrom = selectionRange.from + content.length + 1; - this.#dispatchChange(0, 0, `${content}\n`, selectFrom); - this.focus(); + this.editor.input.spliceText(0, 0, `${content}\n`, {from: selectFrom}); + this.editor.input.focus(); } /** @@ -226,16 +211,15 @@ export class Actions { */ appendContent(content: string): void { content = this.#cleanTextForEditor(content); - const end = this.editor.cm.state.doc.length; - this.#dispatchChange(end, end, `\n${content}`); - this.focus(); + this.editor.input.appendText(content); + this.editor.input.focus(); } /** * Replace the editor's contents */ replaceContent(content: string): void { - this.#setText(content); + this.editor.input.setText(content); } /** @@ -243,17 +227,16 @@ export class Actions { * @param {String} newStart */ replaceLineStart(newStart: string): void { - const selectionRange = this.#getSelectionRange(); - const line = this.editor.cm.state.doc.lineAt(selectionRange.from); - - const lineContent = line.text; + const selectionRange = this.editor.input.getSelection(); + const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from); + const lineContent = this.editor.input.getSelectionText(lineRange); const lineStart = lineContent.split(' ')[0]; // Remove symbol if already set if (lineStart === newStart) { const newLineContent = lineContent.replace(`${newStart} `, ''); const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length); - this.#dispatchChange(line.from, line.to, newLineContent, selectFrom); + this.editor.input.spliceText(selectionRange.from, selectionRange.to, newLineContent, {from: selectFrom}); return; } @@ -266,46 +249,46 @@ export class Actions { } const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length); - this.#dispatchChange(line.from, line.to, newLineContent, selectFrom); + this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectFrom}); } /** * Wrap the selection in the given contents start and end contents. */ wrapSelection(start: string, end: string): void { - const selectRange = this.#getSelectionRange(); - const selectionText = this.#getSelectionText(selectRange); + const selectRange = this.editor.input.getSelection(); + const selectionText = this.editor.input.getSelectionText(selectRange); if (!selectionText) { this.#wrapLine(start, end); return; } - let newSelectionText = selectionText; - let newRange; + let newSelectionText: string; + let newRange = {from: selectRange.from, to: selectRange.to}; if (selectionText.startsWith(start) && selectionText.endsWith(end)) { newSelectionText = selectionText.slice(start.length, selectionText.length - end.length); - newRange = selectRange.extend(selectRange.from, selectRange.to - (start.length + end.length)); + newRange.to = selectRange.to - (start.length + end.length); } else { newSelectionText = `${start}${selectionText}${end}`; - newRange = selectRange.extend(selectRange.from, selectRange.to + (start.length + end.length)); + newRange.to = selectRange.to + (start.length + end.length); } - this.#dispatchChange( + this.editor.input.spliceText( selectRange.from, selectRange.to, newSelectionText, - newRange.anchor, - newRange.head, + newRange, ); } replaceLineStartForOrderedList() { - const selectionRange = this.#getSelectionRange(); - const line = this.editor.cm.state.doc.lineAt(selectionRange.from); - const prevLine = this.editor.cm.state.doc.line(line.number - 1); + const selectionRange = this.editor.input.getSelection(); + const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from); + const prevLineRange = this.editor.input.getLineRangeFromPosition(lineRange.from - 1); + const prevLineText = this.editor.input.getSelectionText(prevLineRange); - const listMatch = prevLine.text.match(/^(\s*)(\d)([).])\s/) || []; + const listMatch = prevLineText.match(/^(\s*)(\d)([).])\s/) || []; const number = (Number(listMatch[2]) || 0) + 1; const whiteSpace = listMatch[1] || ''; @@ -320,30 +303,32 @@ export class Actions { * Creates a callout block if none existing, and removes it if cycling past the danger type. */ cycleCalloutTypeAtSelection() { - const selectionRange = this.#getSelectionRange(); - const line = this.editor.cm.state.doc.lineAt(selectionRange.from); + const selectionRange = this.editor.input.getSelection(); + const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from); + const lineText = this.editor.input.getSelectionText(lineRange); const formats = ['info', 'success', 'warning', 'danger']; const joint = formats.join('|'); const regex = new RegExp(`class="((${joint})\\s+callout|callout\\s+(${joint}))"`, 'i'); - const matches = regex.exec(line.text) || ['']; + const matches = regex.exec(lineText); const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase(); if (format === formats[formats.length - 1]) { this.#wrapLine(`

`, '

'); } else if (format === '') { this.#wrapLine('

', '

'); - } else { + } else if (matches) { const newFormatIndex = formats.indexOf(format) + 1; const newFormat = formats[newFormatIndex]; - const newContent = line.text.replace(matches[0], matches[0].replace(format, newFormat)); - const lineDiff = newContent.length - line.text.length; - this.#dispatchChange( - line.from, - line.to, + const newContent = lineText.replace(matches[0], matches[0].replace(format, newFormat)); + const lineDiff = newContent.length - lineText.length; + const anchor = Math.min(selectionRange.from, selectionRange.to); + const head = Math.max(selectionRange.from, selectionRange.to); + this.editor.input.spliceText( + lineRange.from, + lineRange.to, newContent, - selectionRange.anchor + lineDiff, - selectionRange.head + lineDiff, + {from: anchor + lineDiff, to: head + lineDiff} ); } } @@ -357,8 +342,7 @@ export class Actions { return; } - const blockInfo = this.editor.cm.lineBlockAtHeight(scrollEl.scrollTop); - const range = this.editor.cm.state.sliceDoc(0, blockInfo.from); + const range = this.editor.input.getTextAboveView(); const parser = new DOMParser(); const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html'); const totalLines = doc.documentElement.querySelectorAll('body > *'); @@ -370,10 +354,10 @@ export class Actions { * The page-relative position provided can be used to determine insert location if possible. */ async insertTemplate(templateId: string, posX: number, posY: number): Promise { - const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); + const cursorPos = this.editor.input.coordsToSelection(posX, posY).from; const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string}; const content = responseData.markdown || responseData.html; - this.#dispatchChange(cursorPos, cursorPos, content, cursorPos); + this.editor.input.spliceText(cursorPos, cursorPos, content, {from: cursorPos}); } /** @@ -381,21 +365,21 @@ export class Actions { * screen coordinates (Typically form a paste event). */ insertClipboardImages(images: File[], posX: number, posY: number): void { - const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); + const cursorPos = this.editor.input.coordsToSelection(posX, posY).from; for (const image of images) { this.uploadImage(image, cursorPos); } } /** - * Handle image upload and add image into markdown content + * Handle image upload and add image into Markdown content */ async uploadImage(file: File, position: number|null = null): Promise { if (file === null || file.type.indexOf('image') !== 0) return; let ext = 'png'; if (position === null) { - position = this.#getSelectionRange().from; + position = this.editor.input.getSelection().from; } if (file.name) { @@ -409,7 +393,7 @@ export class Actions { const id = `image-${Math.random().toString(16).slice(2)}`; const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); const placeHolderText = `![](${placeholderImage})`; - this.#dispatchChange(position, position, placeHolderText, position); + this.editor.input.spliceText(position, position, placeHolderText, {from: position}); const remoteFilename = `image-${Date.now()}.${ext}`; const formData = new FormData(); @@ -427,54 +411,16 @@ export class Actions { } } - /** - * Get the current text of the editor instance. - * @return {string} - */ - #getText() { - return this.editor.cm.state.doc.toString(); - } - - /** - * Set the text of the current editor instance. - */ - #setText(text: string, selectionRange: SelectionRange|null = null) { - selectionRange = selectionRange || this.#getSelectionRange(); - const newDoc = this.editor.cm.state.toText(text); - const newSelectFrom = Math.min(selectionRange.from, newDoc.length); - const scrollTop = this.editor.cm.scrollDOM.scrollTop; - this.#dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom); - this.focus(); - window.requestAnimationFrame(() => { - this.editor.cm.scrollDOM.scrollTop = scrollTop; - }); - } - /** * Replace the current selection and focus the editor. * Takes an offset for the cursor, after the change, relative to the start of the provided string. * Can be provided a selection range to use instead of the current selection range. */ - #replaceSelection(newContent: string, cursorOffset: number = 0, selectionRange: SelectionRange|null = null) { - selectionRange = selectionRange || this.#getSelectionRange(); - const selectFrom = selectionRange.from + cursorOffset; - this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom); - this.focus(); - } - - /** - * Get the text content of the main current selection. - */ - #getSelectionText(selectionRange: SelectionRange|null = null): string { - selectionRange = selectionRange || this.#getSelectionRange(); - return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to); - } - - /** - * Get the range of the current main selection. - */ - #getSelectionRange(): SelectionRange { - return this.editor.cm.state.selection.main; + #replaceSelection(newContent: string, offset: number = 0, selection: MarkdownEditorInputSelection|null = null) { + selection = selection || this.editor.input.getSelection(); + const selectFrom = selection.from + offset; + this.editor.input.spliceText(selection.from, selection.to, newContent, {from: selectFrom, to: selectFrom}); + this.editor.input.focus(); } /** @@ -489,19 +435,19 @@ export class Actions { * Find and replace the first occurrence of [search] with [replace] */ #findAndReplaceContent(search: string, replace: string): void { - const newText = this.#getText().replace(search, replace); - this.#setText(newText); + const newText = this.editor.input.getText().replace(search, replace); + this.editor.input.setText(newText); } /** * Wrap the line in the given start and end contents. */ #wrapLine(start: string, end: string): void { - const selectionRange = this.#getSelectionRange(); - const line = this.editor.cm.state.doc.lineAt(selectionRange.from); - const lineContent = line.text; - let newLineContent; - let lineOffset = 0; + const selectionRange = this.editor.input.getSelection(); + const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from); + const lineContent = this.editor.input.getSelectionText(lineRange); + let newLineContent: string; + let lineOffset: number; if (lineContent.startsWith(start) && lineContent.endsWith(end)) { newLineContent = lineContent.slice(start.length, lineContent.length - end.length); @@ -511,44 +457,7 @@ export class Actions { lineOffset = start.length; } - this.#dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset); - } - - /** - * Dispatch changes to the editor. - */ - #dispatchChange(from: number, to: number|null = null, text: string|null = null, selectFrom: number|null = null, selectTo: number|null = null): void { - const change: ChangeSpec = {from}; - if (to) { - change.to = to; - } - if (text) { - change.insert = text; - } - const tr: TransactionSpec = {changes: change}; - - if (selectFrom) { - tr.selection = {anchor: selectFrom}; - if (selectTo) { - tr.selection.head = selectTo; - } - } - - this.editor.cm.dispatch(tr); - } - - /** - * Set the current selection range. - * Optionally will scroll the new range into view. - * @param {Number} from - * @param {Number} to - * @param {Boolean} scrollIntoView - */ - #setSelection(from: number, to: number, scrollIntoView = false) { - this.editor.cm.dispatch({ - selection: {anchor: from, head: to}, - scrollIntoView, - }); + this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectionRange.from + lineOffset}); } } diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index b983285d907..5385e27cc46 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -4,9 +4,9 @@ import {Actions} from './actions'; import {Settings} from './settings'; import {listenToCommonEvents} from './common-events'; import {init as initCodemirror} from './codemirror'; -import {EditorView} from "@codemirror/view"; -import {importVersioned} from "../services/util"; import {CodeModule} from "../global"; +import {MarkdownEditorInput} from "./inputs/interface"; +import {CodemirrorInput} from "./inputs/codemirror"; export interface MarkdownEditorConfig { pageId: string; @@ -23,7 +23,7 @@ export interface MarkdownEditor { display: Display; markdown: Markdown; actions: Actions; - cm: EditorView; + input: MarkdownEditorInput; settings: Settings; } @@ -41,7 +41,9 @@ export async function init(config: MarkdownEditorConfig): Promise { - this.editor.cm.scrollDOM.scrollTop = scrollTop; + this.cm.scrollDOM.scrollTop = scrollTop; }); } - spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection | null = null) { + spliceText(from: number, to: number, newText: string, selection: Partial | null = null) { const end = (selection?.from === selection?.to) ? null : selection?.to; this.dispatchChange(from, to, newText, selection?.from, end) } appendText(text: string) { - const end = this.editor.cm.state.doc.length; + const end = this.cm.state.doc.length; this.dispatchChange(end, end, `\n${text}`); } getLineText(lineIndex: number = -1): string { const index = lineIndex > -1 ? lineIndex : this.getSelection().from; - return this.editor.cm.state.doc.lineAt(index).text; + return this.cm.state.doc.lineAt(index).text; } - wrapLine(start: string, end: string) { - const selectionRange = this.getSelection(); - const line = this.editor.cm.state.doc.lineAt(selectionRange.from); - const lineContent = line.text; - let newLineContent; - let lineOffset = 0; - - if (lineContent.startsWith(start) && lineContent.endsWith(end)) { - newLineContent = lineContent.slice(start.length, lineContent.length - end.length); - lineOffset = -(start.length); - } else { - newLineContent = `${start}${lineContent}${end}`; - lineOffset = start.length; - } + coordsToSelection(x: number, y: number): MarkdownEditorInputSelection { + const cursorPos = this.cm.posAtCoords({x, y}, false); + return {from: cursorPos, to: cursorPos}; + } - this.dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset); + getLineRangeFromPosition(position: number): MarkdownEditorInputSelection { + const line = this.cm.state.doc.lineAt(position); + return {from: line.from, to: line.to}; } - coordsToSelection(x: number, y: number): MarkdownEditorInputSelection { - const cursorPos = this.editor.cm.posAtCoords({x, y}, false); - return {from: cursorPos, to: cursorPos}; + searchForLineContaining(text: string): MarkdownEditorInputSelection | null { + const docText = this.cm.state.doc; + let lineCount = 1; + let scrollToLine = -1; + for (const line of docText.iterLines()) { + if (line.includes(text)) { + scrollToLine = lineCount; + break; + } + lineCount += 1; + } + + if (scrollToLine === -1) { + return null; + } + + const line = docText.line(scrollToLine); + return {from: line.from, to: line.to}; } /** diff --git a/resources/js/markdown/inputs/interface.ts b/resources/js/markdown/inputs/interface.ts index aafd86f9187..c0397ecd09d 100644 --- a/resources/js/markdown/inputs/interface.ts +++ b/resources/js/markdown/inputs/interface.ts @@ -18,12 +18,12 @@ export interface MarkdownEditorInput { /** * Get the text of the given (or current) selection range. */ - getSelectionText(selection: MarkdownEditorInputSelection|null = null): string; + getSelectionText(selection?: MarkdownEditorInputSelection): string; /** * Set the selection range of the editor. */ - setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false): void; + setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void; /** * Get the full text of the input. @@ -40,13 +40,13 @@ export interface MarkdownEditorInput { * Set the full text of the input. * Optionally can provide a selection to restore after setting text. */ - setText(text: string, selection: MarkdownEditorInputSelection|null = null): void; + setText(text: string, selection?: MarkdownEditorInputSelection): void; /** * Splice in/out text within the input. * Optionally can provide a selection to restore after setting text. */ - spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection|null = null): void; + spliceText(from: number, to: number, newText: string, selection: Partial|null): void; /** * Append text to the end of the editor. @@ -57,15 +57,20 @@ export interface MarkdownEditorInput { * Get the text of the given line number otherwise the text * of the current selected line. */ - getLineText(lineIndex:number = -1): string; + getLineText(lineIndex:number): string; /** - * Wrap the current line in the given start/end contents. + * Get a selection representing the line range from the given position. */ - wrapLine(start: string, end: string): void; + getLineRangeFromPosition(position: number): MarkdownEditorInputSelection; /** * Convert the given screen coords to a selection position within the input. */ coordsToSelection(x: number, y: number): MarkdownEditorInputSelection; + + /** + * Search and return a line range which includes the provided text. + */ + searchForLineContaining(text: string): MarkdownEditorInputSelection|null; } \ No newline at end of file From 6b4b500a3313f30c92a8a6ffa1d8427fdf4d2aaa Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 21 Jul 2025 18:53:22 +0100 Subject: [PATCH 05/11] MD Editor: Added plaintext input implementation --- resources/js/markdown/codemirror.ts | 59 +--------- resources/js/markdown/dom-handlers.ts | 62 ++++++++++ resources/js/markdown/index.mts | 18 ++- resources/js/markdown/inputs/textarea.ts | 137 +++++++++++++++++++++++ resources/js/markdown/shortcuts.ts | 8 +- resources/sass/_forms.scss | 3 + 6 files changed, 225 insertions(+), 62 deletions(-) create mode 100644 resources/js/markdown/dom-handlers.ts create mode 100644 resources/js/markdown/inputs/textarea.ts diff --git a/resources/js/markdown/codemirror.ts b/resources/js/markdown/codemirror.ts index 1b54c58196b..82aeb11418f 100644 --- a/resources/js/markdown/codemirror.ts +++ b/resources/js/markdown/codemirror.ts @@ -1,72 +1,19 @@ import {provideKeyBindings} from './shortcuts'; -import {debounce} from '../services/util'; -import {Clipboard} from '../services/clipboard'; import {EditorView, ViewUpdate} from "@codemirror/view"; import {MarkdownEditor} from "./index.mjs"; import {CodeModule} from "../global"; +import {MarkdownEditorEventMap} from "./dom-handlers"; /** - * Initiate the codemirror instance for the MarkDown editor. + * Initiate the codemirror instance for the Markdown editor. */ -export function init(editor: MarkdownEditor, Code: CodeModule): EditorView { +export function init(editor: MarkdownEditor, Code: CodeModule, domEventHandlers: MarkdownEditorEventMap): EditorView { function onViewUpdate(v: ViewUpdate) { if (v.docChanged) { editor.actions.updateAndRender(); } } - const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false); - let syncActive = editor.settings.get('scrollSync'); - editor.settings.onChange('scrollSync', val => { - syncActive = val; - }); - - const domEventHandlers = { - // Handle scroll to sync display view - scroll: (event: Event) => syncActive && onScrollDebounced(event), - // Handle image & content drag n drop - drop: (event: DragEvent) => { - if (!event.dataTransfer) { - return; - } - - const templateId = event.dataTransfer.getData('bookstack/template'); - if (templateId) { - event.preventDefault(); - editor.actions.insertTemplate(templateId, event.pageX, event.pageY); - } - - const clipboard = new Clipboard(event.dataTransfer); - const clipboardImages = clipboard.getImages(); - if (clipboardImages.length > 0) { - event.stopPropagation(); - event.preventDefault(); - editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY); - } - }, - // Handle dragover event to allow as drop-target in chrome - dragover: (event: DragEvent) => { - event.preventDefault(); - }, - // Handle image paste - paste: (event: ClipboardEvent) => { - if (!event.clipboardData) { - return; - } - - const clipboard = new Clipboard(event.clipboardData); - - // Don't handle the event ourselves if no items exist of contains table-looking data - if (!clipboard.hasItems() || clipboard.containsTabularData()) { - return; - } - - const images = clipboard.getImages(); - for (const image of images) { - editor.actions.uploadImage(image); - } - }, - }; const cm = Code.markdownEditor( editor.config.inputEl, diff --git a/resources/js/markdown/dom-handlers.ts b/resources/js/markdown/dom-handlers.ts new file mode 100644 index 00000000000..db3f2b57676 --- /dev/null +++ b/resources/js/markdown/dom-handlers.ts @@ -0,0 +1,62 @@ +import {Clipboard} from "../services/clipboard"; +import {MarkdownEditor} from "./index.mjs"; +import {debounce} from "../services/util"; + + +export type MarkdownEditorEventMap = Record void>; + +export function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEditorEventMap { + + const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false); + let syncActive = editor.settings.get('scrollSync'); + editor.settings.onChange('scrollSync', val => { + syncActive = val; + }); + + return { + // Handle scroll to sync display view + scroll: (event: Event) => syncActive && onScrollDebounced(event), + // Handle image & content drag n drop + drop: (event: DragEvent) => { + if (!event.dataTransfer) { + return; + } + + const templateId = event.dataTransfer.getData('bookstack/template'); + if (templateId) { + event.preventDefault(); + editor.actions.insertTemplate(templateId, event.pageX, event.pageY); + } + + const clipboard = new Clipboard(event.dataTransfer); + const clipboardImages = clipboard.getImages(); + if (clipboardImages.length > 0) { + event.stopPropagation(); + event.preventDefault(); + editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY); + } + }, + // Handle dragover event to allow as drop-target in chrome + dragover: (event: DragEvent) => { + event.preventDefault(); + }, + // Handle image paste + paste: (event: ClipboardEvent) => { + if (!event.clipboardData) { + return; + } + + const clipboard = new Clipboard(event.clipboardData); + + // Don't handle the event ourselves if no items exist of contains table-looking data + if (!clipboard.hasItems() || clipboard.containsTabularData()) { + return; + } + + const images = clipboard.getImages(); + for (const image of images) { + editor.actions.uploadImage(image); + } + }, + }; +} \ No newline at end of file diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index 5385e27cc46..7edf80d4fb4 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -7,6 +7,9 @@ import {init as initCodemirror} from './codemirror'; import {CodeModule} from "../global"; import {MarkdownEditorInput} from "./inputs/interface"; import {CodemirrorInput} from "./inputs/codemirror"; +import {TextareaInput} from "./inputs/textarea"; +import {provideShortcutMap} from "./shortcuts"; +import {getMarkdownDomEventHandlers} from "./dom-handlers"; export interface MarkdownEditorConfig { pageId: string; @@ -31,7 +34,7 @@ export interface MarkdownEditor { * Initiate a new Markdown editor instance. */ export async function init(config: MarkdownEditorConfig): Promise { - const Code = await window.importVersioned('code') as CodeModule; + // const Code = await window.importVersioned('code') as CodeModule; const editor: MarkdownEditor = { config, @@ -42,8 +45,17 @@ export async function init(config: MarkdownEditorConfig): Promise -1) { + return this.getLineRangeFromPosition(textPosition); + } + + return null; + } + + setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void { + this.input.selectionStart = selection.from; + this.input.selectionEnd = selection.to; + } + + setText(text: string, selection?: MarkdownEditorInputSelection): void { + this.input.value = text; + if (selection) { + this.setSelection(selection, false); + } + } + + spliceText(from: number, to: number, newText: string, selection: Partial | null): void { + const text = this.getText(); + const updatedText = text.slice(0, from) + newText + text.slice(to); + this.setText(updatedText); + if (selection && selection.from) { + const newSelection = {from: selection.from, to: selection.to || selection.from}; + this.setSelection(newSelection, false); + } + } +} \ No newline at end of file diff --git a/resources/js/markdown/shortcuts.ts b/resources/js/markdown/shortcuts.ts index c746b52e703..734160f29f0 100644 --- a/resources/js/markdown/shortcuts.ts +++ b/resources/js/markdown/shortcuts.ts @@ -1,11 +1,13 @@ import {MarkdownEditor} from "./index.mjs"; import {KeyBinding} from "@codemirror/view"; +export type MarkdownEditorShortcutMap = Record void>; + /** * Provide shortcuts for the editor instance. */ -function provide(editor: MarkdownEditor): Record void> { - const shortcuts: Record void> = {}; +export function provideShortcutMap(editor: MarkdownEditor): MarkdownEditorShortcutMap { + const shortcuts: MarkdownEditorShortcutMap = {}; // Insert Image shortcut shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage(); @@ -45,7 +47,7 @@ function provide(editor: MarkdownEditor): Record void> { * Get the editor shortcuts in CodeMirror keybinding format. */ export function provideKeyBindings(editor: MarkdownEditor): KeyBinding[] { - const shortcuts = provide(editor); + const shortcuts = provideShortcutMap(editor); const keyBindings = []; const wrapAction = (action: ()=>void) => () => { diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index b66688f8d20..e71edc1d757 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -57,6 +57,9 @@ padding: vars.$xs vars.$m; color: #444; border-radius: 0; + height: 100%; + font-size: 14px; + line-height: 1.2; max-height: 100%; flex: 1; border: 0; From d55db06c01302c8a2e597c91620fbfb8ddfc6982 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 22 Jul 2025 10:34:29 +0100 Subject: [PATCH 06/11] MD Editor: Added plaintext/cm switching Also aligned the construction of the inputs where possible. --- resources/js/markdown/codemirror.ts | 39 +++++++++++++++++----- resources/js/markdown/index.mts | 29 ++++++++++------ resources/js/markdown/inputs/codemirror.ts | 4 +++ resources/js/markdown/inputs/interface.ts | 5 +++ resources/js/markdown/inputs/textarea.ts | 30 ++++++++++++++--- resources/js/markdown/shortcuts.ts | 20 ----------- 6 files changed, 83 insertions(+), 44 deletions(-) diff --git a/resources/js/markdown/codemirror.ts b/resources/js/markdown/codemirror.ts index 82aeb11418f..1ae01847704 100644 --- a/resources/js/markdown/codemirror.ts +++ b/resources/js/markdown/codemirror.ts @@ -1,25 +1,48 @@ -import {provideKeyBindings} from './shortcuts'; -import {EditorView, ViewUpdate} from "@codemirror/view"; -import {MarkdownEditor} from "./index.mjs"; +import {EditorView, KeyBinding, ViewUpdate} from "@codemirror/view"; import {CodeModule} from "../global"; import {MarkdownEditorEventMap} from "./dom-handlers"; +import {MarkdownEditorShortcutMap} from "./shortcuts"; + +/** + * Convert editor shortcuts to CodeMirror keybinding format. + */ +export function shortcutsToKeyBindings(shortcuts: MarkdownEditorShortcutMap): KeyBinding[] { + const keyBindings = []; + + const wrapAction = (action: () => void) => () => { + action(); + return true; + }; + + for (const [shortcut, action] of Object.entries(shortcuts)) { + keyBindings.push({key: shortcut, run: wrapAction(action), preventDefault: true}); + } + + return keyBindings; +} /** * Initiate the codemirror instance for the Markdown editor. */ -export function init(editor: MarkdownEditor, Code: CodeModule, domEventHandlers: MarkdownEditorEventMap): EditorView { +export async function init( + input: HTMLTextAreaElement, + shortcuts: MarkdownEditorShortcutMap, + domEventHandlers: MarkdownEditorEventMap, + onChange: () => void +): Promise { + const Code = await window.importVersioned('code') as CodeModule; + function onViewUpdate(v: ViewUpdate) { if (v.docChanged) { - editor.actions.updateAndRender(); + onChange(); } } - const cm = Code.markdownEditor( - editor.config.inputEl, + input, onViewUpdate, domEventHandlers, - provideKeyBindings(editor), + shortcutsToKeyBindings(shortcuts), ); // Add editor view to the window for easy access/debugging. diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index 7edf80d4fb4..4cd89c0777f 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -4,7 +4,6 @@ import {Actions} from './actions'; import {Settings} from './settings'; import {listenToCommonEvents} from './common-events'; import {init as initCodemirror} from './codemirror'; -import {CodeModule} from "../global"; import {MarkdownEditorInput} from "./inputs/interface"; import {CodemirrorInput} from "./inputs/codemirror"; import {TextareaInput} from "./inputs/textarea"; @@ -34,8 +33,6 @@ export interface MarkdownEditor { * Initiate a new Markdown editor instance. */ export async function init(config: MarkdownEditorConfig): Promise { - // const Code = await window.importVersioned('code') as CodeModule; - const editor: MarkdownEditor = { config, markdown: new Markdown(), @@ -46,15 +43,25 @@ export async function init(config: MarkdownEditorConfig): Promise editor.actions.updateAndRender(); + + const initCodemirrorInput: () => Promise = async () => { + const codeMirror = await initCodemirror(config.inputEl, shortcuts, eventHandlers, onInputChange); + return new CodemirrorInput(codeMirror); + }; + const initTextAreaInput: () => Promise = async () => { + return new TextareaInput(config.inputEl, shortcuts, eventHandlers, onInputChange); + }; + const isPlainEditor = Boolean(editor.settings.get('plainEditor')); + editor.input = await (isPlainEditor ? initTextAreaInput() : initCodemirrorInput()); + editor.settings.onChange('plainEditor', async (value) => { + const isPlain = Boolean(value); + const newInput = await (isPlain ? initTextAreaInput() : initCodemirrorInput()); + editor.input.teardown(); + editor.input = newInput; + }); // window.devinput = editor.input; listenToCommonEvents(editor); diff --git a/resources/js/markdown/inputs/codemirror.ts b/resources/js/markdown/inputs/codemirror.ts index 029d238fe85..3ab219a6330 100644 --- a/resources/js/markdown/inputs/codemirror.ts +++ b/resources/js/markdown/inputs/codemirror.ts @@ -10,6 +10,10 @@ export class CodemirrorInput implements MarkdownEditorInput { this.cm = cm; } + teardown(): void { + this.cm.destroy(); + } + focus(): void { if (!this.cm.hasFocus) { this.cm.focus(); diff --git a/resources/js/markdown/inputs/interface.ts b/resources/js/markdown/inputs/interface.ts index c0397ecd09d..66a8c07e798 100644 --- a/resources/js/markdown/inputs/interface.ts +++ b/resources/js/markdown/inputs/interface.ts @@ -73,4 +73,9 @@ export interface MarkdownEditorInput { * Search and return a line range which includes the provided text. */ searchForLineContaining(text: string): MarkdownEditorInputSelection|null; + + /** + * Tear down the input. + */ + teardown(): void; } \ No newline at end of file diff --git a/resources/js/markdown/inputs/textarea.ts b/resources/js/markdown/inputs/textarea.ts index d1eabd27027..25c8779fc73 100644 --- a/resources/js/markdown/inputs/textarea.ts +++ b/resources/js/markdown/inputs/textarea.ts @@ -8,23 +8,43 @@ export class TextareaInput implements MarkdownEditorInput { protected input: HTMLTextAreaElement; protected shortcuts: MarkdownEditorShortcutMap; protected events: MarkdownEditorEventMap; - - constructor(input: HTMLTextAreaElement, shortcuts: MarkdownEditorShortcutMap, events: MarkdownEditorEventMap) { + protected onChange: () => void; + protected eventController = new AbortController(); + + constructor( + input: HTMLTextAreaElement, + shortcuts: MarkdownEditorShortcutMap, + events: MarkdownEditorEventMap, + onChange: () => void + ) { this.input = input; this.shortcuts = shortcuts; this.events = events; + this.onChange = onChange; this.onKeyDown = this.onKeyDown.bind(this); this.configureListeners(); + + this.input.style.removeProperty("display"); + } + + teardown() { + this.eventController.abort('teardown'); } configureListeners(): void { - // TODO - Teardown handling - this.input.addEventListener('keydown', this.onKeyDown); + // Keyboard shortcuts + this.input.addEventListener('keydown', this.onKeyDown, {signal: this.eventController.signal}); + // Shared event listeners for (const [name, listener] of Object.entries(this.events)) { - this.input.addEventListener(name, listener); + this.input.addEventListener(name, listener, {signal: this.eventController.signal}); } + + // Input change handling + this.input.addEventListener('input', () => { + this.onChange(); + }, {signal: this.eventController.signal}); } onKeyDown(e: KeyboardEvent) { diff --git a/resources/js/markdown/shortcuts.ts b/resources/js/markdown/shortcuts.ts index 734160f29f0..175e8f4f04b 100644 --- a/resources/js/markdown/shortcuts.ts +++ b/resources/js/markdown/shortcuts.ts @@ -1,5 +1,4 @@ import {MarkdownEditor} from "./index.mjs"; -import {KeyBinding} from "@codemirror/view"; export type MarkdownEditorShortcutMap = Record void>; @@ -42,22 +41,3 @@ export function provideShortcutMap(editor: MarkdownEditor): MarkdownEditorShortc return shortcuts; } - -/** - * Get the editor shortcuts in CodeMirror keybinding format. - */ -export function provideKeyBindings(editor: MarkdownEditor): KeyBinding[] { - const shortcuts = provideShortcutMap(editor); - const keyBindings = []; - - const wrapAction = (action: ()=>void) => () => { - action(); - return true; - }; - - for (const [shortcut, action] of Object.entries(shortcuts)) { - keyBindings.push({key: shortcut, run: wrapAction(action), preventDefault: true}); - } - - return keyBindings; -} From 6621d55f3d2248a273554c0f780dbeef465e6b15 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 22 Jul 2025 16:42:47 +0100 Subject: [PATCH 07/11] MD Editor: Worked to improve/fix positioning code Still pending testing. Old logic did not work when lines would wrap, so changing things to a character/line measuring technique. Fixed some other isues too while testing shortcuts. --- resources/js/markdown/actions.ts | 10 +- resources/js/markdown/dom-handlers.ts | 4 +- resources/js/markdown/index.mts | 2 +- resources/js/markdown/inputs/codemirror.ts | 4 +- resources/js/markdown/inputs/interface.ts | 4 +- resources/js/markdown/inputs/textarea.ts | 103 +++++++++++++++++++-- 6 files changed, 108 insertions(+), 19 deletions(-) diff --git a/resources/js/markdown/actions.ts b/resources/js/markdown/actions.ts index ed4ee5904ba..36d21ab1dc6 100644 --- a/resources/js/markdown/actions.ts +++ b/resources/js/markdown/actions.ts @@ -236,7 +236,7 @@ export class Actions { if (lineStart === newStart) { const newLineContent = lineContent.replace(`${newStart} `, ''); const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length); - this.editor.input.spliceText(selectionRange.from, selectionRange.to, newLineContent, {from: selectFrom}); + this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectFrom}); return; } @@ -353,8 +353,8 @@ export class Actions { * Fetch and insert the template of the given ID. * The page-relative position provided can be used to determine insert location if possible. */ - async insertTemplate(templateId: string, posX: number, posY: number): Promise { - const cursorPos = this.editor.input.coordsToSelection(posX, posY).from; + async insertTemplate(templateId: string, event: MouseEvent): Promise { + const cursorPos = this.editor.input.eventToPosition(event).from; const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string}; const content = responseData.markdown || responseData.html; this.editor.input.spliceText(cursorPos, cursorPos, content, {from: cursorPos}); @@ -364,8 +364,8 @@ export class Actions { * Insert multiple images from the clipboard from an event at the provided * screen coordinates (Typically form a paste event). */ - insertClipboardImages(images: File[], posX: number, posY: number): void { - const cursorPos = this.editor.input.coordsToSelection(posX, posY).from; + insertClipboardImages(images: File[], event: MouseEvent): void { + const cursorPos = this.editor.input.eventToPosition(event).from; for (const image of images) { this.uploadImage(image, cursorPos); } diff --git a/resources/js/markdown/dom-handlers.ts b/resources/js/markdown/dom-handlers.ts index db3f2b57676..37e1723de35 100644 --- a/resources/js/markdown/dom-handlers.ts +++ b/resources/js/markdown/dom-handlers.ts @@ -25,7 +25,7 @@ export function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEdi const templateId = event.dataTransfer.getData('bookstack/template'); if (templateId) { event.preventDefault(); - editor.actions.insertTemplate(templateId, event.pageX, event.pageY); + editor.actions.insertTemplate(templateId, event); } const clipboard = new Clipboard(event.dataTransfer); @@ -33,7 +33,7 @@ export function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEdi if (clipboardImages.length > 0) { event.stopPropagation(); event.preventDefault(); - editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY); + editor.actions.insertClipboardImages(clipboardImages, event); } }, // Handle dragover event to allow as drop-target in chrome diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index 4cd89c0777f..7538c197255 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -62,7 +62,7 @@ export async function init(config: MarkdownEditorConfig): Promise void; protected eventController = new AbortController(); + protected textSizeCache: {x: number; y: number}|null = null; + constructor( input: HTMLTextAreaElement, shortcuts: MarkdownEditorShortcutMap, @@ -25,6 +27,8 @@ export class TextareaInput implements MarkdownEditorInput { this.onKeyDown = this.onKeyDown.bind(this); this.configureListeners(); + // TODO - Undo/Redo + this.input.style.removeProperty("display"); } @@ -45,15 +49,24 @@ export class TextareaInput implements MarkdownEditorInput { this.input.addEventListener('input', () => { this.onChange(); }, {signal: this.eventController.signal}); + + this.input.addEventListener('click', (event: MouseEvent) => { + const x = event.clientX; + const y = event.clientY; + const range = this.eventToPosition(event); + const text = this.getText().split(''); + console.log(range, text.slice(0, 20)); + }); } onKeyDown(e: KeyboardEvent) { const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone"; + const key = e.key.length > 1 ? e.key : e.key.toLowerCase(); const keyParts = [ e.shiftKey ? 'Shift' : null, isApple && e.metaKey ? 'Mod' : null, !isApple && e.ctrlKey ? 'Mod' : null, - e.key, + key, ]; const keyString = keyParts.filter(Boolean).join('-'); @@ -65,10 +78,37 @@ export class TextareaInput implements MarkdownEditorInput { appendText(text: string): void { this.input.value += `\n${text}`; + this.input.dispatchEvent(new Event('input')); } - coordsToSelection(x: number, y: number): MarkdownEditorInputSelection { - // TODO + eventToPosition(event: MouseEvent): MarkdownEditorInputSelection { + const eventCoords = this.mouseEventToTextRelativeCoords(event); + const textSize = this.measureTextSize(); + const lineWidth = this.measureLineCharCount(textSize.x); + + const lines = this.getText().split('\n'); + + // TODO - Check this + + let currY = 0; + let currPos = 0; + for (const line of lines) { + let linePos = 0; + const wrapCount = Math.max(Math.ceil(line.length / lineWidth), 1); + for (let i = 0; i < wrapCount; i++) { + currY += textSize.y; + if (currY > eventCoords.y) { + const targetX = Math.floor(eventCoords.x / textSize.x); + const maxPos = Math.min(currPos + linePos + targetX, currPos + line.length); + return {from: maxPos, to: maxPos}; + } + + linePos += lineWidth; + } + + currPos += line.length + 1; + } + return this.getSelection(); } @@ -81,11 +121,11 @@ export class TextareaInput implements MarkdownEditorInput { let lineStart = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; - const newEnd = lineStart + line.length + 1; - if (position < newEnd) { - return {from: lineStart, to: newEnd}; + const lineEnd = lineStart + line.length; + if (position <= lineEnd) { + return {from: lineStart, to: lineEnd}; } - lineStart = newEnd; + lineStart = lineEnd + 1; } return {from: 0, to: 0}; @@ -140,6 +180,7 @@ export class TextareaInput implements MarkdownEditorInput { setText(text: string, selection?: MarkdownEditorInputSelection): void { this.input.value = text; + this.input.dispatchEvent(new Event('input')); if (selection) { this.setSelection(selection, false); } @@ -154,4 +195,52 @@ export class TextareaInput implements MarkdownEditorInput { this.setSelection(newSelection, false); } } + + protected measureTextSize(): {x: number; y: number} { + if (this.textSizeCache) { + return this.textSizeCache; + } + + const el = document.createElement("div"); + el.textContent = `a\nb`; + const inputStyles = window.getComputedStyle(this.input) + el.style.font = inputStyles.font; + el.style.lineHeight = inputStyles.lineHeight; + el.style.padding = '0px'; + el.style.display = 'inline-block'; + el.style.visibility = 'hidden'; + el.style.position = 'absolute'; + el.style.whiteSpace = 'pre'; + this.input.after(el); + + const bounds = el.getBoundingClientRect(); + el.remove(); + this.textSizeCache = { + x: bounds.width, + y: bounds.height / 2, + }; + return this.textSizeCache; + } + + protected measureLineCharCount(textWidth: number): number { + const inputStyles = window.getComputedStyle(this.input); + const paddingLeft = Number(inputStyles.paddingLeft.replace('px', '')); + const paddingRight = Number(inputStyles.paddingRight.replace('px', '')); + const width = Number(inputStyles.width.replace('px', '')); + const textSpace = width - (paddingLeft + paddingRight); + + return Math.floor(textSpace / textWidth); + } + + protected mouseEventToTextRelativeCoords(event: MouseEvent): {x: number; y: number} { + const inputBounds = this.input.getBoundingClientRect(); + const inputStyles = window.getComputedStyle(this.input); + const paddingTop = Number(inputStyles.paddingTop.replace('px', '')); + const paddingLeft = Number(inputStyles.paddingLeft.replace('px', '')); + + const xPos = Math.max(event.clientX - (inputBounds.left + paddingLeft), 0); + const yPos = Math.max((event.clientY - (inputBounds.top + paddingTop)) + this.input.scrollTop, 0); + + return {x: xPos, y: yPos}; + } } \ No newline at end of file From 7ca8bdc23172c258fe1466d803e4992a3551ee06 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 23 Jul 2025 12:17:36 +0100 Subject: [PATCH 08/11] MD Editor: Added custom textarea undo/redo, updated positioning methods --- resources/js/markdown/inputs/textarea.ts | 161 ++++++++++++++++------- resources/sass/_forms.scss | 1 + 2 files changed, 116 insertions(+), 46 deletions(-) diff --git a/resources/js/markdown/inputs/textarea.ts b/resources/js/markdown/inputs/textarea.ts index a80054ee24a..e0c3ac37cf2 100644 --- a/resources/js/markdown/inputs/textarea.ts +++ b/resources/js/markdown/inputs/textarea.ts @@ -1,7 +1,70 @@ import {MarkdownEditorInput, MarkdownEditorInputSelection} from "./interface"; import {MarkdownEditorShortcutMap} from "../shortcuts"; import {MarkdownEditorEventMap} from "../dom-handlers"; +import {debounce} from "../../services/util"; +type UndoStackEntry = { + content: string; + selection: MarkdownEditorInputSelection; +} + +class UndoStack { + protected onChangeDebounced: (callback: () => UndoStackEntry) => void; + + protected stack: UndoStackEntry[] = []; + protected pointer: number = -1; + protected lastActionTime: number = 0; + + constructor() { + this.onChangeDebounced = debounce(this.onChange, 1000, false); + } + + undo(): UndoStackEntry|null { + if (this.pointer < 1) { + return null; + } + + this.lastActionTime = Date.now(); + this.pointer -= 1; + return this.stack[this.pointer]; + } + + redo(): UndoStackEntry|null { + const atEnd = this.pointer === this.stack.length - 1; + if (atEnd) { + return null; + } + + this.lastActionTime = Date.now(); + this.pointer++; + return this.stack[this.pointer]; + } + + push(getValueCallback: () => UndoStackEntry): void { + // Ignore changes made via undo/redo actions + if (Date.now() - this.lastActionTime < 100) { + return; + } + + this.onChangeDebounced(getValueCallback); + } + + protected onChange(getValueCallback: () => UndoStackEntry) { + // Trim the end of the stack from the pointer since we're branching away + if (this.pointer !== this.stack.length - 1) { + this.stack = this.stack.slice(0, this.pointer) + } + + this.stack.push(getValueCallback()); + + // Limit stack size + if (this.stack.length > 50) { + this.stack = this.stack.slice(this.stack.length - 50); + } + + this.pointer = this.stack.length - 1; + } +} export class TextareaInput implements MarkdownEditorInput { @@ -10,6 +73,7 @@ export class TextareaInput implements MarkdownEditorInput { protected events: MarkdownEditorEventMap; protected onChange: () => void; protected eventController = new AbortController(); + protected undoStack = new UndoStack(); protected textSizeCache: {x: number; y: number}|null = null; @@ -25,17 +89,34 @@ export class TextareaInput implements MarkdownEditorInput { this.onChange = onChange; this.onKeyDown = this.onKeyDown.bind(this); + this.configureLocalShortcuts(); this.configureListeners(); - // TODO - Undo/Redo - this.input.style.removeProperty("display"); + this.undoStack.push(() => ({content: this.getText(), selection: this.getSelection()})); } teardown() { this.eventController.abort('teardown'); } + configureLocalShortcuts(): void { + this.shortcuts['Mod-z'] = () => { + const undoEntry = this.undoStack.undo(); + if (undoEntry) { + this.setText(undoEntry.content); + this.setSelection(undoEntry.selection, false); + } + }; + this.shortcuts['Mod-y'] = () => { + const redoContent = this.undoStack.redo(); + if (redoContent) { + this.setText(redoContent.content); + this.setSelection(redoContent.selection, false); + } + } + } + configureListeners(): void { // Keyboard shortcuts this.input.addEventListener('keydown', this.onKeyDown, {signal: this.eventController.signal}); @@ -48,15 +129,8 @@ export class TextareaInput implements MarkdownEditorInput { // Input change handling this.input.addEventListener('input', () => { this.onChange(); + this.undoStack.push(() => ({content: this.input.value, selection: this.getSelection()})); }, {signal: this.eventController.signal}); - - this.input.addEventListener('click', (event: MouseEvent) => { - const x = event.clientX; - const y = event.clientY; - const range = this.eventToPosition(event); - const text = this.getText().split(''); - console.log(range, text.slice(0, 20)); - }); } onKeyDown(e: KeyboardEvent) { @@ -83,33 +157,7 @@ export class TextareaInput implements MarkdownEditorInput { eventToPosition(event: MouseEvent): MarkdownEditorInputSelection { const eventCoords = this.mouseEventToTextRelativeCoords(event); - const textSize = this.measureTextSize(); - const lineWidth = this.measureLineCharCount(textSize.x); - - const lines = this.getText().split('\n'); - - // TODO - Check this - - let currY = 0; - let currPos = 0; - for (const line of lines) { - let linePos = 0; - const wrapCount = Math.max(Math.ceil(line.length / lineWidth), 1); - for (let i = 0; i < wrapCount; i++) { - currY += textSize.y; - if (currY > eventCoords.y) { - const targetX = Math.floor(eventCoords.x / textSize.x); - const maxPos = Math.min(currPos + linePos + targetX, currPos + line.length); - return {from: maxPos, to: maxPos}; - } - - linePos += lineWidth; - } - - currPos += line.length + 1; - } - - return this.getSelection(); + return this.inputPositionToSelection(eventCoords.x, eventCoords.y); } focus(): void { @@ -153,15 +201,8 @@ export class TextareaInput implements MarkdownEditorInput { getTextAboveView(): string { const scrollTop = this.input.scrollTop; - const computedStyles = window.getComputedStyle(this.input); - const lines = this.getText().split('\n'); - const paddingTop = Number(computedStyles.paddingTop.replace('px', '')); - const paddingBottom = Number(computedStyles.paddingBottom.replace('px', '')); - - const avgLineHeight = (this.input.scrollHeight - paddingBottom - paddingTop) / lines.length; - const roughLinePos = Math.max(Math.floor((scrollTop - paddingTop) / avgLineHeight), 0); - const linesAbove = this.getText().split('\n').slice(0, roughLinePos); - return linesAbove.join('\n'); + const selection = this.inputPositionToSelection(0, scrollTop); + return this.getSelectionText({from: 0, to: selection.to}); } searchForLineContaining(text: string): MarkdownEditorInputSelection | null { @@ -243,4 +284,32 @@ export class TextareaInput implements MarkdownEditorInput { return {x: xPos, y: yPos}; } + + protected inputPositionToSelection(x: number, y: number): MarkdownEditorInputSelection { + const textSize = this.measureTextSize(); + const lineWidth = this.measureLineCharCount(textSize.x); + + const lines = this.getText().split('\n'); + + let currY = 0; + let currPos = 0; + for (const line of lines) { + let linePos = 0; + const wrapCount = Math.max(Math.ceil(line.length / lineWidth), 1); + for (let i = 0; i < wrapCount; i++) { + currY += textSize.y; + if (currY > y) { + const targetX = Math.floor(x / textSize.x); + const maxPos = Math.min(currPos + linePos + targetX, currPos + line.length); + return {from: maxPos, to: maxPos}; + } + + linePos += lineWidth; + } + + currPos += line.length + 1; + } + + return this.getSelection(); + } } \ No newline at end of file diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index e71edc1d757..c16f0609499 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -64,6 +64,7 @@ flex: 1; border: 0; width: 100%; + margin: 0; &:focus { outline: 0; } From 53f32849a9fceefab4bc2029c3b7790655184861 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 23 Jul 2025 14:49:41 +0100 Subject: [PATCH 09/11] MD Editor: Last tests/check over plaintext use/switching --- resources/js/markdown/index.mts | 1 - resources/js/services/util.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index 7538c197255..0a6e974b74a 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -62,7 +62,6 @@ export async function init(config: MarkdownEditorConfig): Promise any>(func: T, waitMs: number, immediate: boolean): T { let timeout: number|null = null; return function debouncedWrapper(this: any, ...args: any[]) { const context: any = this; @@ -19,7 +19,7 @@ export function debounce(func: Function, waitMs: number, immediate: boolean): Fu } timeout = window.setTimeout(later, waitMs); if (callNow) func.apply(context, args); - }; + } as T; } function isDetailsElement(element: HTMLElement): element is HTMLDetailsElement { From 3b9c0b34ae811d14ae39d014fc9e427dafe8aad5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 23 Jul 2025 14:59:26 +0100 Subject: [PATCH 10/11] MD Editor: Fixed plaintext dark styles, updated npm packages --- package-lock.json | 691 ++++++++++++++++++++----------------- resources/sass/_forms.scss | 3 +- 2 files changed, 368 insertions(+), 326 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0348fd1ed42..079e397700a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,9 +77,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", - "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -87,22 +87,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.3.tgz", - "integrity": "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.3", - "@babel/parser": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.3", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -118,16 +118,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", - "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.3", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -151,6 +151,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -223,27 +233,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.3.tgz", - "integrity": "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", - "integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -507,38 +517,28 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.3.tgz", - "integrity": "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.3", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -626,9 +626,9 @@ } }, "node_modules/@codemirror/lang-json": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", - "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -636,9 +636,9 @@ } }, "node_modules/@codemirror/lang-markdown": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz", - "integrity": "sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.3.tgz", + "integrity": "sha512-1fn1hQAPWlSSMCvnF810AkhWpNLkJpl66CRfIy3vVl20Sl4NwChkorCHqpMtNbXr1EuMJsrDnhEpjZxKZ2UX3A==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.7.1", @@ -651,9 +651,9 @@ } }, "node_modules/@codemirror/lang-php": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz", - "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", "license": "MIT", "dependencies": { "@codemirror/lang-html": "^6.0.0", @@ -678,9 +678,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz", - "integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", + "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -732,9 +732,9 @@ } }, "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", - "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -744,12 +744,13 @@ } }, "node_modules/@codemirror/view": { - "version": "6.36.8", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.8.tgz", - "integrity": "sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==", + "version": "6.38.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", + "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } @@ -779,9 +780,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -796,9 +797,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -813,9 +814,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -830,9 +831,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -847,9 +848,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -864,9 +865,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -881,9 +882,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -898,9 +899,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -915,9 +916,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -932,9 +933,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -949,9 +950,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -966,9 +967,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -983,9 +984,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -1000,9 +1001,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -1017,9 +1018,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -1034,9 +1035,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -1051,9 +1052,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -1068,9 +1069,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -1085,9 +1086,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -1102,9 +1103,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -1119,9 +1120,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -1135,10 +1136,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -1153,9 +1171,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -1170,9 +1188,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -1187,9 +1205,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -1246,9 +1264,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1261,9 +1279,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1271,9 +1289,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1308,9 +1326,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -1331,13 +1349,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -1817,18 +1835,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1841,27 +1855,17 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1876,9 +1880,9 @@ "license": "MIT" }, "node_modules/@lezer/css": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.2.1.tgz", - "integrity": "sha512-2F5tOqzKEKbCUNraIXc0f6HKeyKlmMWJnBB0i4XW6dJgssrZO/YlZ2pY5xgyqDleqqhiNJ3dQhbrV2aClZQMvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", + "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1887,9 +1891,9 @@ } }, "node_modules/@lezer/generator": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.3.tgz", - "integrity": "sha512-vAI2O1tPF8QMMgp+bdUeeJCneJNkOZvqsrtyb4ohnFVFdboSqPwBEacnt0HH4E+5h+qsIwTHUSAhffU4hzKl1A==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.8.0.tgz", + "integrity": "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1962,9 +1966,9 @@ } }, "node_modules/@lezer/php": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.2.tgz", - "integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.4.tgz", + "integrity": "sha512-D2dJ0t8Z28/G1guztRczMFvPDUqzeMLSQbdWQmaiHV7urc8NlEOnjYk9UrZ531OcLiRxD4Ihcbv7AsDpNKDRaQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2433,9 +2437,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -2535,12 +2539,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/sortablejs": { @@ -2587,9 +2591,9 @@ "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -2749,18 +2753,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3028,9 +3034,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3051,9 +3057,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, "funding": [ { @@ -3071,8 +3077,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -3184,9 +3190,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "dev": true, "funding": [ { @@ -3343,9 +3349,9 @@ } }, "node_modules/codemirror": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -3584,9 +3590,9 @@ } }, "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, "license": "MIT" }, @@ -3770,9 +3776,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.158", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz", - "integrity": "sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ==", + "version": "1.5.190", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", + "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", "dev": true, "license": "ISC" }, @@ -3819,9 +3825,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.10", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", - "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3852,7 +3858,9 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", @@ -3867,6 +3875,7 @@ "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -3965,9 +3974,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3978,31 +3987,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escalade": { @@ -4051,19 +4061,19 @@ } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4075,9 +4085,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4134,9 +4144,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -4162,30 +4172,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -4206,9 +4216,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4223,9 +4233,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4249,15 +4259,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4430,9 +4440,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4519,15 +4529,16 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4970,9 +4981,9 @@ } }, "node_modules/immutable": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", - "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", "dev": true, "license": "MIT" }, @@ -5303,6 +5314,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7349,9 +7373,9 @@ } }, "node_modules/parse5/node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7876,9 +7900,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.89.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz", - "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==", + "version": "1.89.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", + "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", "dev": true, "license": "MIT", "dependencies": { @@ -8029,9 +8053,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { @@ -8250,6 +8274,20 @@ "node": ">=8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -8528,16 +8566,15 @@ } }, "node_modules/ts-jest": { - "version": "29.3.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", - "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "version": "29.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", @@ -8553,10 +8590,11 @@ }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { @@ -8574,6 +8612,9 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, @@ -8837,9 +8878,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, "node_modules/universalify": { @@ -9225,9 +9266,9 @@ } }, "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index c16f0609499..12fb3385f96 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -55,7 +55,8 @@ font-style: normal; font-weight: 400; padding: vars.$xs vars.$m; - color: #444; + @include mixins.lightDark(color, #444, #aaa); + @include mixins.lightDark(background-color, #fff, #222); border-radius: 0; height: 100%; font-size: 14px; From 2668aae09b93feb95ac81aceb19e296879052dbb Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 23 Jul 2025 15:41:55 +0100 Subject: [PATCH 11/11] TypeScript: Updated compile target, addressed issues --- resources/js/wysiwyg/lexical/core/LexicalNode.ts | 2 +- resources/js/wysiwyg/lexical/core/nodes/LexicalDecoratorNode.ts | 2 +- resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts | 2 +- resources/js/wysiwyg/lexical/core/nodes/LexicalLineBreakNode.ts | 2 +- resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts | 2 +- resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts | 2 +- tsconfig.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/LexicalNode.ts b/resources/js/wysiwyg/lexical/core/LexicalNode.ts index 7306e6bca27..6d79c01ccb1 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalNode.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalNode.ts @@ -175,7 +175,7 @@ export type NodeKey = string; export class LexicalNode { // Allow us to look up the type including static props - ['constructor']!: KlassConstructor; + declare ['constructor']: KlassConstructor; /** @internal */ __type: string; /** @internal */ diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalDecoratorNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalDecoratorNode.ts index 99d2669d92f..5015f593ed9 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalDecoratorNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalDecoratorNode.ts @@ -24,7 +24,7 @@ export interface DecoratorNode { /** @noInheritDoc */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class DecoratorNode extends LexicalNode { - ['constructor']!: KlassConstructor>; + declare ['constructor']: KlassConstructor>; constructor(key?: NodeKey) { super(key); } diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts index 9ad50841141..a2760377368 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts @@ -55,7 +55,7 @@ export interface ElementNode { /** @noInheritDoc */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class ElementNode extends LexicalNode { - ['constructor']!: KlassConstructor; + declare ['constructor']: KlassConstructor; /** @internal */ __first: null | NodeKey; /** @internal */ diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalLineBreakNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalLineBreakNode.ts index 2d28db08c12..b1746e7f842 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalLineBreakNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalLineBreakNode.ts @@ -22,7 +22,7 @@ export type SerializedLineBreakNode = SerializedLexicalNode; /** @noInheritDoc */ export class LineBreakNode extends LexicalNode { - ['constructor']!: KlassConstructor; + declare ['constructor']: KlassConstructor; static getType(): string { return 'linebreak'; } diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts index e8d044b218b..6711936da00 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts @@ -44,7 +44,7 @@ export type SerializedParagraphNode = Spread< /** @noInheritDoc */ export class ParagraphNode extends CommonBlockNode { - ['constructor']!: KlassConstructor; + declare ['constructor']: KlassConstructor; /** @internal */ __textFormat: number; __textStyle: string; diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts index 9a486749421..35cc073a0ba 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts @@ -284,7 +284,7 @@ export interface TextNode { /** @noInheritDoc */ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class TextNode extends LexicalNode { - ['constructor']!: KlassConstructor; + declare ['constructor']: KlassConstructor; __text: string; /** @internal */ __format: number; diff --git a/tsconfig.json b/tsconfig.json index 8bffc25f898..dacaefea279 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "include": ["resources/js/**/*"], "exclude": ["resources/js/wysiwyg/lexical/yjs/*"], "compilerOptions": { - "target": "es2019", + "target": "es2022", "module": "commonjs", "rootDir": "./resources/js/", "baseUrl": "./",