@sciflow/editor-start 0.0.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -29
- package/dist/bundle/sciflow-editor.css +1 -1
- package/dist/bundle/sciflow-editor.js +4020 -3326
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib/editor-element.d.ts +97 -1
- package/dist/lib/editor-element.d.ts.map +1 -1
- package/dist/lib/editor-element.js +435 -25
- package/dist/lib/format-bar.d.ts +29 -0
- package/dist/lib/format-bar.d.ts.map +1 -1
- package/dist/lib/format-bar.js +53 -3
- package/dist/lib/outline.d.ts +44 -0
- package/dist/lib/outline.d.ts.map +1 -1
- package/dist/lib/outline.js +120 -6
- package/dist/lib/range-decorations.d.ts +120 -0
- package/dist/lib/range-decorations.d.ts.map +1 -0
- package/dist/lib/range-decorations.js +131 -0
- package/dist/lib/read-only.d.ts +95 -0
- package/dist/lib/read-only.d.ts.map +1 -0
- package/dist/lib/read-only.js +123 -0
- package/dist/lib/selection-editor.d.ts +103 -2
- package/dist/lib/selection-editor.d.ts.map +1 -1
- package/dist/lib/selection-editor.js +222 -13
- package/dist/testing/external-widget-simulator.d.ts +217 -0
- package/dist/testing/external-widget-simulator.d.ts.map +1 -0
- package/dist/testing/external-widget-simulator.js +350 -0
- package/package.json +25 -13
- package/dist/lib/test-fixtures/soak-documents.d.ts +0 -13
- package/dist/lib/test-fixtures/soak-documents.d.ts.map +0 -1
- package/dist/lib/test-fixtures/soak-documents.js +0 -69
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module external-widget-simulator
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* INTERNAL TESTING UTILITY — NOT STABLE PUBLIC API.
|
|
6
|
+
* This module is exported from the `@sciflow/editor-start` package solely to
|
|
7
|
+
* enable automated tests and the interactive demo page to verify light-DOM
|
|
8
|
+
* reachability. Its API may change without a semver notice. Do not depend on
|
|
9
|
+
* it in production code.
|
|
10
|
+
*
|
|
11
|
+
* A vanilla-JS simulator that models the three key behaviours of browser
|
|
12
|
+
* proofreading and AI writing extensions (e.g. Grammarly, the LanguageTool
|
|
13
|
+
* browser add-on, DeepL Write, etc.) when they interact with a page's
|
|
14
|
+
* editable surface.
|
|
15
|
+
*
|
|
16
|
+
* Design constraints
|
|
17
|
+
* ------------------
|
|
18
|
+
* - Zero runtime dependencies — only public web APIs.
|
|
19
|
+
* - Never traverses into any ShadowRoot. Extensions operate exclusively on
|
|
20
|
+
* the flat document tree; they have no access to shadow-DOM internals.
|
|
21
|
+
* - Suitable for both automated tests (jsdom / vitest) and manual in-browser
|
|
22
|
+
* testing via the demo page.
|
|
23
|
+
*
|
|
24
|
+
* The three capabilities modelled
|
|
25
|
+
* --------------------------------
|
|
26
|
+
* 1. `discover(root?)` — walks the light DOM for editable targets exactly as a
|
|
27
|
+
* real extension would: `querySelectorAll` on [contenteditable], textarea,
|
|
28
|
+
* and text inputs, then filters to nodes whose root is the document (i.e.
|
|
29
|
+
* not behind a shadow boundary).
|
|
30
|
+
*
|
|
31
|
+
* 2. `markRange(target, from, to, options?)` — draws underline-style overlay
|
|
32
|
+
* markers over a character range inside a text node the way widgets render
|
|
33
|
+
* grammar/spell highlights. Uses Range + getClientRects() and places
|
|
34
|
+
* absolutely-positioned <span> elements in an overlay layer that is a
|
|
35
|
+
* sibling of the editable (never inside it).
|
|
36
|
+
*
|
|
37
|
+
* 3. DOM mutation operations that mimic extensions touching the editable
|
|
38
|
+
* directly, bypassing the editor's own transaction mechanism:
|
|
39
|
+
* - `injectMarker(target, from, to)` — wraps a text sub-range in a
|
|
40
|
+
* `<span data-extn-marker="1">` (how widgets tag matches for hover
|
|
41
|
+
* interactions without accepting/rejecting them yet).
|
|
42
|
+
* - `applyCorrection(target, from, to, replacement)` — replaces a text
|
|
43
|
+
* sub-range via raw Range manipulation (the "accept suggestion" action).
|
|
44
|
+
*
|
|
45
|
+
* Threat/usage model
|
|
46
|
+
* ------------------
|
|
47
|
+
* A ProseMirror contenteditable mounted inside a shadow root is INVISIBLE to
|
|
48
|
+
* extensions because they do not call `shadowRoot.querySelector`. This
|
|
49
|
+
* simulator encodes that exact distinction: `discover()` returns only light-DOM
|
|
50
|
+
* editables whose `getRootNode() === document`. The fact that a slotted
|
|
51
|
+
* element satisfies this condition while a shadow-buried element does not is
|
|
52
|
+
* the core regression guard for the light-DOM spike (Option B).
|
|
53
|
+
*
|
|
54
|
+
* jsdom limitation
|
|
55
|
+
* ----------------
|
|
56
|
+
* `getClientRects()` is not implemented in jsdom and always returns an empty
|
|
57
|
+
* DOMRectList. `markRange()` therefore cannot be meaningfully tested in the
|
|
58
|
+
* standard vitest suite — it is exercised through the manual demo page instead.
|
|
59
|
+
* `discover()`, `injectMarker()`, and `applyCorrection()` are all jsdom-safe
|
|
60
|
+
* and form the automated test surface.
|
|
61
|
+
*/
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// 1. discover
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* Walk the light DOM for editable targets, exactly as browser extensions do.
|
|
67
|
+
*
|
|
68
|
+
* Extensions use `document.querySelectorAll` (or equivalent TreeWalker
|
|
69
|
+
* traversals) to find `[contenteditable]`, `textarea`, and `input[type=text]`.
|
|
70
|
+
* Critically, they do NOT call `shadowRoot.querySelectorAll` or otherwise
|
|
71
|
+
* traverse into shadow trees — they only see what is in the flat document tree.
|
|
72
|
+
*
|
|
73
|
+
* This function replicates that: it queries from `root` (defaulting to
|
|
74
|
+
* `document`) and then filters the results to those whose `getRootNode()` is
|
|
75
|
+
* the passed `root`. An editable that lives inside a shadow root will NOT
|
|
76
|
+
* satisfy this condition and will NOT be returned.
|
|
77
|
+
*
|
|
78
|
+
* @param root The root to query from. Defaults to `document`.
|
|
79
|
+
* @returns An array of `EditableTarget` objects for each discovered field.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Asserts the editor's contenteditable is reachable from the document root:
|
|
83
|
+
* const targets = discover(document);
|
|
84
|
+
* const ce = targets.find(t => t.element.hasAttribute('contenteditable'));
|
|
85
|
+
* assert(ce?.isLightDom === true);
|
|
86
|
+
* assert(ce?.rootNode === document);
|
|
87
|
+
*/
|
|
88
|
+
export function discover(root = document) {
|
|
89
|
+
// Determine both the query root and the "expected" root node.
|
|
90
|
+
//
|
|
91
|
+
// We cannot reliably use `instanceof Document` across different window
|
|
92
|
+
// contexts (e.g. jsdom creates its own Document subclass that does not share
|
|
93
|
+
// the identity of the global `Document`). Instead we duck-type: a Document
|
|
94
|
+
// has no `ownerDocument` of its own, while an Element does.
|
|
95
|
+
//
|
|
96
|
+
// queryRoot — the node we call querySelectorAll on (must support that API).
|
|
97
|
+
// expectedRoot — the node we expect `el.getRootNode()` to equal for a
|
|
98
|
+
// light-DOM element. For a Document this is the Document
|
|
99
|
+
// itself; for an Element it is the Element's ownerDocument.
|
|
100
|
+
const isDocument = !('ownerDocument' in root && root.ownerDocument !== null);
|
|
101
|
+
const queryRoot = isDocument
|
|
102
|
+
? root
|
|
103
|
+
: (root.ownerDocument ?? root);
|
|
104
|
+
const expectedRoot = isDocument
|
|
105
|
+
? root
|
|
106
|
+
: (root.ownerDocument ?? root);
|
|
107
|
+
const results = [];
|
|
108
|
+
// Selector mirrors what real extensions use:
|
|
109
|
+
// - [contenteditable] covers both contenteditable="true" and contenteditable=""
|
|
110
|
+
// - textarea
|
|
111
|
+
// - input[type=text] (and inputs without an explicit type, which default to text)
|
|
112
|
+
const elements = queryRoot.querySelectorAll('[contenteditable], textarea, input[type=text], input:not([type])');
|
|
113
|
+
for (const el of Array.from(elements)) {
|
|
114
|
+
const rootNode = el.getRootNode();
|
|
115
|
+
results.push({
|
|
116
|
+
element: el,
|
|
117
|
+
rootNode,
|
|
118
|
+
isLightDom: rootNode === expectedRoot,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// 2. markRange (overlay positioning — in-browser only; jsdom returns empty rects)
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
/**
|
|
127
|
+
* Position overlay underline markers over a character range inside `target`.
|
|
128
|
+
*
|
|
129
|
+
* Mimics how extensions draw underlines: they create a DOM Range, call
|
|
130
|
+
* `getClientRects()` to obtain the line boxes, and then absolutely-position
|
|
131
|
+
* overlay `<span>` elements in a sibling container at those coordinates.
|
|
132
|
+
* Critically, the overlay is NOT inserted inside the contenteditable — it is
|
|
133
|
+
* a sibling to avoid invalidating the editable's DOM tree from the extension's
|
|
134
|
+
* perspective.
|
|
135
|
+
*
|
|
136
|
+
* NOTE: `getClientRects()` always returns an empty list in jsdom. This
|
|
137
|
+
* function is a no-op in that environment and is intended for manual
|
|
138
|
+
* in-browser verification via the demo page. The automated test suite
|
|
139
|
+
* tests `discover`, `injectMarker`, and `applyCorrection` instead.
|
|
140
|
+
*
|
|
141
|
+
* @param target The editable element to overlay.
|
|
142
|
+
* @param from Character offset (from the start of `target.textContent`) of
|
|
143
|
+
* the range start.
|
|
144
|
+
* @param to Character offset of the range end.
|
|
145
|
+
* @param options Visual options.
|
|
146
|
+
* @returns The overlay container element, or `null` when `getClientRects`
|
|
147
|
+
* returned nothing (jsdom / hidden element).
|
|
148
|
+
*/
|
|
149
|
+
export function markRange(target, from, to, options = {}) {
|
|
150
|
+
const { color = '#ef4444', offsetY = 2, zIndex = '9999' } = options;
|
|
151
|
+
const range = resolveCharRange(target, from, to);
|
|
152
|
+
if (!range)
|
|
153
|
+
return null;
|
|
154
|
+
const rects = Array.from(range.getClientRects());
|
|
155
|
+
if (rects.length === 0)
|
|
156
|
+
return null; // jsdom / invisible element
|
|
157
|
+
// Ensure the overlay container exists as a sibling of the editable.
|
|
158
|
+
let overlay = target.parentElement?.querySelector('[data-extn-overlay]');
|
|
159
|
+
if (!overlay) {
|
|
160
|
+
overlay = document.createElement('div');
|
|
161
|
+
overlay.setAttribute('data-extn-overlay', '');
|
|
162
|
+
overlay.style.cssText = `
|
|
163
|
+
position: fixed;
|
|
164
|
+
top: 0;
|
|
165
|
+
left: 0;
|
|
166
|
+
width: 0;
|
|
167
|
+
height: 0;
|
|
168
|
+
overflow: visible;
|
|
169
|
+
pointer-events: none;
|
|
170
|
+
z-index: ${zIndex};
|
|
171
|
+
`;
|
|
172
|
+
target.parentElement?.appendChild(overlay);
|
|
173
|
+
}
|
|
174
|
+
for (const rect of rects) {
|
|
175
|
+
const marker = document.createElement('span');
|
|
176
|
+
marker.setAttribute('data-extn-underline', '');
|
|
177
|
+
marker.style.cssText = `
|
|
178
|
+
position: fixed;
|
|
179
|
+
left: ${rect.left}px;
|
|
180
|
+
top: ${rect.bottom + offsetY}px;
|
|
181
|
+
width: ${rect.width}px;
|
|
182
|
+
height: 2px;
|
|
183
|
+
background: ${color};
|
|
184
|
+
border-radius: 1px;
|
|
185
|
+
pointer-events: none;
|
|
186
|
+
`;
|
|
187
|
+
overlay.appendChild(marker);
|
|
188
|
+
}
|
|
189
|
+
return overlay;
|
|
190
|
+
}
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// 3a. injectMarker
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
/**
|
|
195
|
+
* Wrap a character sub-range of `target` in a `<span data-extn-marker="1">`.
|
|
196
|
+
*
|
|
197
|
+
* This is what extensions do to tag a match for subsequent accept/reject
|
|
198
|
+
* interactions — they insert a wrapper span *inside* the contenteditable via
|
|
199
|
+
* raw DOM Range manipulation without going through the editor's transaction
|
|
200
|
+
* system.
|
|
201
|
+
*
|
|
202
|
+
* From ProseMirror's perspective this is a foreign DOM mutation. ProseMirror's
|
|
203
|
+
* DOMObserver will observe the mutation and attempt to reconcile its internal
|
|
204
|
+
* model. The mutation-tolerance tests in the spec verify that this does not
|
|
205
|
+
* corrupt the document or cause PM to throw.
|
|
206
|
+
*
|
|
207
|
+
* @param target The contenteditable element to mutate.
|
|
208
|
+
* @param from Character offset of the range start (within `target.textContent`).
|
|
209
|
+
* @param to Character offset of the range end.
|
|
210
|
+
* @returns An `InjectedMarker` handle with a `remove()` method for cleanup.
|
|
211
|
+
* Returns `null` when the range cannot be resolved.
|
|
212
|
+
*/
|
|
213
|
+
export function injectMarker(target, from, to) {
|
|
214
|
+
const range = resolveCharRange(target, from, to);
|
|
215
|
+
if (!range)
|
|
216
|
+
return null;
|
|
217
|
+
const span = document.createElement('span');
|
|
218
|
+
span.setAttribute('data-extn-marker', '1');
|
|
219
|
+
try {
|
|
220
|
+
range.surroundContents(span);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// surroundContents throws if the range crosses element boundaries.
|
|
224
|
+
// Fall back to extracting and wrapping manually.
|
|
225
|
+
const fragment = range.extractContents();
|
|
226
|
+
span.appendChild(fragment);
|
|
227
|
+
range.insertNode(span);
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
span,
|
|
231
|
+
remove() {
|
|
232
|
+
if (!span.parentNode)
|
|
233
|
+
return;
|
|
234
|
+
const frag = document.createDocumentFragment();
|
|
235
|
+
while (span.firstChild)
|
|
236
|
+
frag.appendChild(span.firstChild);
|
|
237
|
+
span.parentNode.replaceChild(frag, span);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// 3b. applyCorrection
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
/**
|
|
245
|
+
* Replace a character sub-range of `target` with `replacement` via raw DOM.
|
|
246
|
+
*
|
|
247
|
+
* This is the "accept suggestion" operation: the extension overwrites a word
|
|
248
|
+
* by creating a Range, deleting its contents, and inserting a new text node.
|
|
249
|
+
* It does NOT fire a ProseMirror transaction.
|
|
250
|
+
*
|
|
251
|
+
* The test that calls this should follow up with a normal PM transaction (e.g.
|
|
252
|
+
* `view.dispatch(view.state.tr.insertText(...))`) to verify that ProseMirror
|
|
253
|
+
* reconciles the foreign mutation cleanly and does not produce corrupted or
|
|
254
|
+
* duplicated content.
|
|
255
|
+
*
|
|
256
|
+
* @param target The contenteditable element.
|
|
257
|
+
* @param from Character offset of the range to replace.
|
|
258
|
+
* @param to Character offset of the range end.
|
|
259
|
+
* @param replacement The text to insert in place of the matched range.
|
|
260
|
+
* @returns A `CorrectionResult` describing what was swapped, or
|
|
261
|
+
* `null` when the range cannot be resolved.
|
|
262
|
+
*/
|
|
263
|
+
export function applyCorrection(target, from, to, replacement) {
|
|
264
|
+
const range = resolveCharRange(target, from, to);
|
|
265
|
+
if (!range)
|
|
266
|
+
return null;
|
|
267
|
+
const replaced = range.toString();
|
|
268
|
+
range.deleteContents();
|
|
269
|
+
range.insertNode(document.createTextNode(replacement));
|
|
270
|
+
return { replaced, inserted: replacement };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Find all occurrences of `searchString` within `target.textContent` and
|
|
274
|
+
* return their character-offset ranges.
|
|
275
|
+
*
|
|
276
|
+
* This is the bridge between "a word I want to underline" and the `{from, to}`
|
|
277
|
+
* pair that `markRange`, `injectMarker`, and `applyCorrection` consume.
|
|
278
|
+
* Extensions compute positions exactly this way: read the full `textContent`
|
|
279
|
+
* of the editable as one flat string, locate every match, then use those
|
|
280
|
+
* offsets to build DOM Ranges.
|
|
281
|
+
*
|
|
282
|
+
* @param target The element whose `textContent` is searched.
|
|
283
|
+
* @param searchString The literal string to find (case-sensitive).
|
|
284
|
+
* @returns Array of `{from, to}` ranges, one per occurrence,
|
|
285
|
+
* in document order. Empty array when there are no matches.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* const hits = findTextOccurrences(editable, 'SciFlow');
|
|
289
|
+
* for (const { from, to } of hits) {
|
|
290
|
+
* markRange(editable, from, to, { color: '#f97316' });
|
|
291
|
+
* }
|
|
292
|
+
*/
|
|
293
|
+
export function findTextOccurrences(target, searchString) {
|
|
294
|
+
if (!searchString)
|
|
295
|
+
return [];
|
|
296
|
+
const text = target.textContent ?? '';
|
|
297
|
+
const results = [];
|
|
298
|
+
let pos = 0;
|
|
299
|
+
while (pos <= text.length - searchString.length) {
|
|
300
|
+
const idx = text.indexOf(searchString, pos);
|
|
301
|
+
if (idx === -1)
|
|
302
|
+
break;
|
|
303
|
+
results.push({ from: idx, to: idx + searchString.length });
|
|
304
|
+
pos = idx + 1; // advance by 1 to find overlapping matches too
|
|
305
|
+
}
|
|
306
|
+
return results;
|
|
307
|
+
}
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Internal helpers
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
/**
|
|
312
|
+
* Resolve a character-offset range within `target.textContent` to a DOM
|
|
313
|
+
* `Range` object, walking the element's text nodes.
|
|
314
|
+
*
|
|
315
|
+
* Character offsets are counted across all text nodes in document order,
|
|
316
|
+
* which matches how extensions typically calculate positions (they read
|
|
317
|
+
* `textContent` as a single string, then locate the text node containing
|
|
318
|
+
* each offset).
|
|
319
|
+
*/
|
|
320
|
+
function resolveCharRange(target, from, to) {
|
|
321
|
+
const walker = document.createTreeWalker(target, NodeFilter.SHOW_TEXT);
|
|
322
|
+
let offset = 0;
|
|
323
|
+
let startNode = null;
|
|
324
|
+
let startOffset = 0;
|
|
325
|
+
let endNode = null;
|
|
326
|
+
let endOffset = 0;
|
|
327
|
+
let node;
|
|
328
|
+
// eslint-disable-next-line no-cond-assign
|
|
329
|
+
while ((node = walker.nextNode())) {
|
|
330
|
+
const text = node;
|
|
331
|
+
const len = text.nodeValue?.length ?? 0;
|
|
332
|
+
if (startNode === null && offset + len > from) {
|
|
333
|
+
startNode = text;
|
|
334
|
+
startOffset = from - offset;
|
|
335
|
+
}
|
|
336
|
+
if (endNode === null && offset + len >= to) {
|
|
337
|
+
endNode = text;
|
|
338
|
+
endOffset = to - offset;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
offset += len;
|
|
342
|
+
}
|
|
343
|
+
// Also handle the case where `to` falls exactly at the start of a node.
|
|
344
|
+
if (startNode === null || endNode === null)
|
|
345
|
+
return null;
|
|
346
|
+
const range = document.createRange();
|
|
347
|
+
range.setStart(startNode, startOffset);
|
|
348
|
+
range.setEnd(endNode, endOffset);
|
|
349
|
+
return range;
|
|
350
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sciflow/editor-start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"private": false,
|
|
5
6
|
"homepage": "https://docs.sciflow.org",
|
|
6
7
|
"type": "module",
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|
|
28
|
+
"!dist/**/*.fixtures.*",
|
|
29
|
+
"!dist/**/*.tsbuildinfo",
|
|
27
30
|
"bin",
|
|
28
31
|
"THIRD_PARTY_LICENSES"
|
|
29
32
|
],
|
|
@@ -49,11 +52,12 @@
|
|
|
49
52
|
"bundle": {
|
|
50
53
|
"executor": "nx:run-commands",
|
|
51
54
|
"outputs": [
|
|
52
|
-
"{projectRoot}/dist/bundle"
|
|
55
|
+
"{projectRoot}/dist/bundle",
|
|
56
|
+
"{projectRoot}/demo/build"
|
|
53
57
|
],
|
|
54
58
|
"options": {
|
|
55
59
|
"cwd": "packages/editor/start",
|
|
56
|
-
"command": "echo 'Building bundle...' && vite build --config vite.config.ts --mode production && echo 'Bundle complete!'"
|
|
60
|
+
"command": "echo 'Building bundle...' && vite build --config vite.config.ts --mode production && echo 'Building demo import bundle...' && vite build --config vite.import.config.ts --mode production && echo 'Bundle complete!'"
|
|
57
61
|
},
|
|
58
62
|
"configurations": {
|
|
59
63
|
"watch": {
|
|
@@ -72,7 +76,7 @@
|
|
|
72
76
|
"cwd": "packages/editor/start",
|
|
73
77
|
"parallel": true,
|
|
74
78
|
"commands": [
|
|
75
|
-
"tsc --build
|
|
79
|
+
"tsc --build ../core/tsconfig.lib.json --watch",
|
|
76
80
|
"vite build --config vite.config.ts --watch",
|
|
77
81
|
"vite dev --open"
|
|
78
82
|
]
|
|
@@ -92,24 +96,32 @@
|
|
|
92
96
|
}
|
|
93
97
|
},
|
|
94
98
|
"dependencies": {
|
|
99
|
+
"@sciflow/schema-core": "0.1.1",
|
|
95
100
|
"lit": "^3.2.1",
|
|
96
101
|
"prosemirror-commands": "1.7.1",
|
|
97
102
|
"prosemirror-keymap": "1.2.3",
|
|
98
|
-
"prosemirror-state": "1.4.4",
|
|
99
|
-
"prosemirror-view": "1.41.8",
|
|
100
103
|
"tslib": "^2.3.0"
|
|
101
104
|
},
|
|
102
105
|
"peerDependencies": {
|
|
103
|
-
"@sciflow/editor-core": "0.
|
|
104
|
-
"@sciflow/schema-prosemirror": "0.
|
|
105
|
-
"prosemirror-model": "
|
|
106
|
+
"@sciflow/editor-core": "0.1.1",
|
|
107
|
+
"@sciflow/schema-prosemirror": "0.1.1",
|
|
108
|
+
"prosemirror-model": "1.25.8",
|
|
109
|
+
"prosemirror-state": "1.4.4",
|
|
110
|
+
"prosemirror-view": "1.41.8"
|
|
106
111
|
},
|
|
107
112
|
"devDependencies": {
|
|
113
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
114
|
+
"@codemirror/lang-xml": "^6.1.0",
|
|
115
|
+
"@codemirror/theme-one-dark": "^6.1.3",
|
|
108
116
|
"@material-symbols/svg-400": "^0.44.0",
|
|
109
|
-
"@sciflow/editor-core": "0.
|
|
110
|
-
"@sciflow/
|
|
117
|
+
"@sciflow/editor-core": "0.1.1",
|
|
118
|
+
"@sciflow/pandoc-web": "0.1.1",
|
|
119
|
+
"@sciflow/schema-prosemirror": "0.1.1",
|
|
120
|
+
"codemirror": "^6.0.2",
|
|
111
121
|
"jsdom": "^29.0.2",
|
|
112
|
-
"prosemirror-model": "1.25.
|
|
113
|
-
"vitest": "^4.1.3"
|
|
122
|
+
"prosemirror-model": "1.25.8",
|
|
123
|
+
"vitest": "^4.1.3",
|
|
124
|
+
"prosemirror-state": "1.4.4",
|
|
125
|
+
"prosemirror-view": "1.41.8"
|
|
114
126
|
}
|
|
115
127
|
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { SciFlowDocJSON } from '@sciflow/editor-core';
|
|
2
|
-
type SoakDocSize = {
|
|
3
|
-
headingCount: number;
|
|
4
|
-
paragraphsPerHeading: number;
|
|
5
|
-
wordsPerParagraph: number;
|
|
6
|
-
figureEvery: number;
|
|
7
|
-
};
|
|
8
|
-
export declare const createSoakDocFixture: (size: SoakDocSize) => SciFlowDocJSON;
|
|
9
|
-
export declare const SMALL_SOAK_DOC: SciFlowDocJSON;
|
|
10
|
-
export declare const MEDIUM_SOAK_DOC: SciFlowDocJSON;
|
|
11
|
-
export declare const LARGE_SOAK_DOC: SciFlowDocJSON;
|
|
12
|
-
export {};
|
|
13
|
-
//# sourceMappingURL=soak-documents.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"soak-documents.d.ts","sourceRoot":"","sources":["../../../src/lib/test-fixtures/soak-documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,KAAK,WAAW,GAAG;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAoCF,eAAO,MAAM,oBAAoB,GAAI,MAAM,WAAW,KAAG,cAwBxD,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,cAK3B,CAAC;AAEH,eAAO,MAAM,eAAe,EAAE,cAK5B,CAAC;AAEH,eAAO,MAAM,cAAc,EAAE,cAK3B,CAAC"}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
const WORD = 'sciflow';
|
|
2
|
-
const paragraphText = (wordCount) => Array.from({ length: wordCount }, () => WORD).join(' ');
|
|
3
|
-
const createParagraph = (wordCount) => ({
|
|
4
|
-
type: 'paragraph',
|
|
5
|
-
attrs: { id: null },
|
|
6
|
-
content: [{ type: 'text', text: paragraphText(wordCount) }],
|
|
7
|
-
});
|
|
8
|
-
const createFigure = (id, caption) => ({
|
|
9
|
-
type: 'figure',
|
|
10
|
-
attrs: {
|
|
11
|
-
id,
|
|
12
|
-
src: `https://example.com/${id}.png`,
|
|
13
|
-
alt: caption,
|
|
14
|
-
type: 'figure',
|
|
15
|
-
orientation: 'portrait',
|
|
16
|
-
},
|
|
17
|
-
content: [
|
|
18
|
-
{
|
|
19
|
-
type: 'caption',
|
|
20
|
-
content: [
|
|
21
|
-
{
|
|
22
|
-
type: 'paragraph',
|
|
23
|
-
attrs: { id: null },
|
|
24
|
-
content: [{ type: 'text', text: caption }],
|
|
25
|
-
},
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
});
|
|
30
|
-
export const createSoakDocFixture = (size) => {
|
|
31
|
-
const content = [];
|
|
32
|
-
for (let headingIndex = 0; headingIndex < size.headingCount; headingIndex += 1) {
|
|
33
|
-
const headingId = `section-${headingIndex + 1}`;
|
|
34
|
-
content.push({
|
|
35
|
-
type: 'heading',
|
|
36
|
-
attrs: { level: ((headingIndex % 3) + 1), id: headingId },
|
|
37
|
-
content: [{ type: 'text', text: `Section ${headingIndex + 1}` }],
|
|
38
|
-
});
|
|
39
|
-
for (let paragraphIndex = 0; paragraphIndex < size.paragraphsPerHeading; paragraphIndex += 1) {
|
|
40
|
-
content.push(createParagraph(size.wordsPerParagraph));
|
|
41
|
-
}
|
|
42
|
-
if ((headingIndex + 1) % size.figureEvery === 0) {
|
|
43
|
-
const figureId = `figure-${headingIndex + 1}`;
|
|
44
|
-
content.push(createFigure(figureId, `Figure for ${headingId}`));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
type: 'doc',
|
|
49
|
-
content,
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
export const SMALL_SOAK_DOC = createSoakDocFixture({
|
|
53
|
-
headingCount: 6,
|
|
54
|
-
paragraphsPerHeading: 2,
|
|
55
|
-
wordsPerParagraph: 25,
|
|
56
|
-
figureEvery: 3,
|
|
57
|
-
});
|
|
58
|
-
export const MEDIUM_SOAK_DOC = createSoakDocFixture({
|
|
59
|
-
headingCount: 20,
|
|
60
|
-
paragraphsPerHeading: 4,
|
|
61
|
-
wordsPerParagraph: 40,
|
|
62
|
-
figureEvery: 2,
|
|
63
|
-
});
|
|
64
|
-
export const LARGE_SOAK_DOC = createSoakDocFixture({
|
|
65
|
-
headingCount: 60,
|
|
66
|
-
paragraphsPerHeading: 6,
|
|
67
|
-
wordsPerParagraph: 50,
|
|
68
|
-
figureEvery: 2,
|
|
69
|
-
});
|