@bendyline/squisq-editor-react 1.6.2 → 2.0.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/README.md +12 -3
- package/dist/index.d.ts +1275 -183
- package/dist/index.js +20345 -12777
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +1607 -241
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +39 -33
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +349 -261
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +107 -14
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +362 -89
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/RecorderEntry.tsx +2 -0
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +12 -3
- package/src/TemplatePicker.tsx +75 -21
- package/src/ThemeCustomizerPanel.tsx +8 -0
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +357 -51
- package/src/TransitionPicker.tsx +36 -4
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/WysiwygEditor.tsx +124 -91
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +51 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +123 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +0 -12
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +15 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaAttachmentFlow.test.ts +2 -2
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +382 -5
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/selectionConversions.test.ts +80 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +71 -7
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +254 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +284 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
- package/src/asciiDiagram/asciiDiagramData.ts +149 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +238 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/codeContext/types.ts +1 -1
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +25 -6
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/frontmatterSettings.ts +23 -0
- package/src/headingTransition.ts +0 -15
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +198 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +48 -18
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +5 -1
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +119 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +17 -3
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +21 -7
- package/src/scene/layers/edgeGeometry.ts +9 -1
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +5 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +13 -4
- package/src/scene/tools/SceneTool.ts +8 -0
- package/src/scene/tools/SelectTool.ts +39 -23
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +596 -0
- package/src/styles/editor.css +736 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +189 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +12 -5
- package/src/timeline/TimelineEditorWidget.tsx +861 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
- package/src/timeline/__tests__/timelineOps.test.ts +277 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +421 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +381 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +47 -21
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +390 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +177 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
- package/src/treeview/treeOps.ts +231 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +99 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -272
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { createElement, type JSX } from 'react';
|
|
21
|
-
import type { SceneTool, SceneToolContext } from './SceneTool';
|
|
21
|
+
import type { SceneInteractionState, SceneTool, SceneToolContext } from './SceneTool';
|
|
22
22
|
import type { SceneCommand } from '../commands/SceneCommand';
|
|
23
23
|
import type { ResizeCorner } from '../SceneSelection';
|
|
24
24
|
|
|
@@ -53,7 +53,13 @@ type DragState = MoveDragState | MarqueeDragState | ResizeDragState;
|
|
|
53
53
|
const DRAG_THRESHOLD_PX = 3;
|
|
54
54
|
const MIN_LAYER_SIZE = 8;
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
function getDragState(interaction: SceneInteractionState): DragState | null {
|
|
57
|
+
return (interaction.selectDrag as DragState | undefined) ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setDragState(interaction: SceneInteractionState, state: DragState | null): void {
|
|
61
|
+
interaction.selectDrag = state ?? undefined;
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
function pointerCoords(e: React.PointerEvent): { sx: number; sy: number } {
|
|
59
65
|
const rect = (e.currentTarget as Element).getBoundingClientRect();
|
|
@@ -90,20 +96,23 @@ function applyResize(
|
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
/** Begin a handle-initiated resize drag. Called by the Scene when a corner is pressed. */
|
|
93
|
-
export function beginHandleDrag(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
export function beginHandleDrag(
|
|
100
|
+
args: {
|
|
101
|
+
layerId: string;
|
|
102
|
+
corner: ResizeCorner;
|
|
103
|
+
startV: { x: number; y: number };
|
|
104
|
+
startBounds: { x: number; y: number; width: number; height: number };
|
|
105
|
+
},
|
|
106
|
+
interaction: SceneInteractionState,
|
|
107
|
+
): void {
|
|
108
|
+
setDragState(interaction, {
|
|
100
109
|
kind: 'resize',
|
|
101
110
|
layerId: args.layerId,
|
|
102
111
|
corner: args.corner,
|
|
103
112
|
startV: args.startV,
|
|
104
113
|
startBounds: args.startBounds,
|
|
105
114
|
currentBounds: args.startBounds,
|
|
106
|
-
};
|
|
115
|
+
});
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
export const SelectTool: SceneTool = {
|
|
@@ -135,29 +144,30 @@ export const SelectTool: SceneTool = {
|
|
|
135
144
|
const origin = getLayerOrigin(ctx, id);
|
|
136
145
|
if (origin) layerStarts.set(id, origin);
|
|
137
146
|
}
|
|
138
|
-
|
|
147
|
+
setDragState(ctx.interaction, {
|
|
139
148
|
kind: 'move',
|
|
140
149
|
startV: v,
|
|
141
150
|
startS: { x: sx, y: sy },
|
|
142
151
|
layerStarts,
|
|
143
152
|
currentV: v,
|
|
144
153
|
movedPastThreshold: false,
|
|
145
|
-
};
|
|
154
|
+
});
|
|
146
155
|
} else {
|
|
147
156
|
if (!e.shiftKey) ctx.setSelection([]);
|
|
148
|
-
|
|
157
|
+
setDragState(ctx.interaction, {
|
|
149
158
|
kind: 'marquee',
|
|
150
159
|
startV: v,
|
|
151
160
|
startS: { x: sx, y: sy },
|
|
152
161
|
currentV: v,
|
|
153
162
|
movedPastThreshold: false,
|
|
154
|
-
};
|
|
163
|
+
});
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
158
167
|
},
|
|
159
168
|
|
|
160
169
|
onPointerMove(e, ctx) {
|
|
170
|
+
const dragState = getDragState(ctx.interaction);
|
|
161
171
|
if (!dragState) return;
|
|
162
172
|
const { sx, sy } = pointerCoords(e);
|
|
163
173
|
const v = ctx.screenToViewport(sx, sy);
|
|
@@ -182,6 +192,7 @@ export const SelectTool: SceneTool = {
|
|
|
182
192
|
},
|
|
183
193
|
|
|
184
194
|
onPointerUp(e, ctx) {
|
|
195
|
+
const dragState = getDragState(ctx.interaction);
|
|
185
196
|
if (!dragState) return;
|
|
186
197
|
if (
|
|
187
198
|
dragState.kind === 'move' &&
|
|
@@ -201,14 +212,14 @@ export const SelectTool: SceneTool = {
|
|
|
201
212
|
}
|
|
202
213
|
} else if (dragState.kind === 'resize') {
|
|
203
214
|
const { x, y, width, height } = dragState.currentBounds;
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
ctx.dispatch({ kind: 'resizeLayer', id: dragState.layerId, width, height });
|
|
215
|
+
// Keep the final origin + size in one command. In particular, the ASCII
|
|
216
|
+
// diagram adapter must render/reparse only once: committing a move and
|
|
217
|
+
// then a resize used to normalize the art between the two halves of one
|
|
218
|
+
// pointer gesture and could snap the node to a different grid position.
|
|
219
|
+
ctx.dispatch({ kind: 'resizeLayer', id: dragState.layerId, x, y, width, height });
|
|
209
220
|
}
|
|
210
221
|
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
|
211
|
-
|
|
222
|
+
setDragState(ctx.interaction, null);
|
|
212
223
|
},
|
|
213
224
|
|
|
214
225
|
onDoubleClick(e, ctx) {
|
|
@@ -232,7 +243,8 @@ export const SelectTool: SceneTool = {
|
|
|
232
243
|
}
|
|
233
244
|
},
|
|
234
245
|
|
|
235
|
-
renderOverlay(): JSX.Element | null {
|
|
246
|
+
renderOverlay(ctx): JSX.Element | null {
|
|
247
|
+
const dragState = getDragState(ctx.interaction);
|
|
236
248
|
if (!dragState || dragState.kind !== 'marquee' || !dragState.movedPastThreshold) return null;
|
|
237
249
|
const box = boxFromCorners(dragState.startV, dragState.currentV);
|
|
238
250
|
return createElement('rect', {
|
|
@@ -247,7 +259,10 @@ export const SelectTool: SceneTool = {
|
|
|
247
259
|
};
|
|
248
260
|
|
|
249
261
|
/** Return the active move-drag offset (viewport units). Null if no drag, or if the drag is a marquee/resize. */
|
|
250
|
-
export function getActiveMoveOffset(
|
|
262
|
+
export function getActiveMoveOffset(
|
|
263
|
+
interaction: SceneInteractionState,
|
|
264
|
+
): { dx: number; dy: number } | null {
|
|
265
|
+
const dragState = getDragState(interaction);
|
|
251
266
|
if (!dragState || dragState.kind !== 'move' || !dragState.movedPastThreshold) return null;
|
|
252
267
|
return {
|
|
253
268
|
dx: dragState.currentV.x - dragState.startV.x,
|
|
@@ -256,10 +271,11 @@ export function getActiveMoveOffset(): { dx: number; dy: number } | null {
|
|
|
256
271
|
}
|
|
257
272
|
|
|
258
273
|
/** Return the live bounds for the layer currently being resized. */
|
|
259
|
-
export function getActiveResize(): {
|
|
274
|
+
export function getActiveResize(interaction: SceneInteractionState): {
|
|
260
275
|
layerId: string;
|
|
261
276
|
bounds: { x: number; y: number; width: number; height: number };
|
|
262
277
|
} | null {
|
|
278
|
+
const dragState = getDragState(interaction);
|
|
263
279
|
if (!dragState || dragState.kind !== 'resize') return null;
|
|
264
280
|
return { layerId: dragState.layerId, bounds: dragState.currentBounds };
|
|
265
281
|
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* ShapeTool — drag to draw a rectangle or circle as a new ShapeLayer.
|
|
3
3
|
*
|
|
4
4
|
* Exposed as a factory so a host can pre-bind the shape kind and
|
|
5
|
-
* stroke/fill defaults. Returns a fresh SceneTool instance
|
|
6
|
-
*
|
|
7
|
-
* sharing module-level drag state.
|
|
5
|
+
* stroke/fill defaults. Returns a fresh SceneTool instance so hosts can bind
|
|
6
|
+
* independent tool identities and defaults; gesture state belongs to Scene.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { createElement, type JSX } from 'react';
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-text selection conversions used by the toolbar's Convert menu.
|
|
3
|
+
* Keeping delimiter detection separate from the editor integrations makes
|
|
4
|
+
* the behavior identical in Monaco and Tiptap.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type SelectionTableDelimiter = 'pipe' | 'comma' | 'tab' | 'multispace' | null;
|
|
8
|
+
|
|
9
|
+
export interface SelectionTable {
|
|
10
|
+
delimiter: SelectionTableDelimiter;
|
|
11
|
+
rows: string[][];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SelectionTaskItem {
|
|
15
|
+
checked: boolean;
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CandidateDelimiter = Exclude<SelectionTableDelimiter, null>;
|
|
20
|
+
|
|
21
|
+
/** Non-empty selected lines, trimmed without changing their order. */
|
|
22
|
+
export function selectionLines(text: string): string[] {
|
|
23
|
+
return text
|
|
24
|
+
.replace(/\r\n?/g, '\n')
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter((line) => line.length > 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function splitPipeLine(line: string): string[] {
|
|
31
|
+
let value = line.trim();
|
|
32
|
+
if (value.startsWith('|')) value = value.slice(1);
|
|
33
|
+
if (value.endsWith('|') && !value.endsWith('\\|')) value = value.slice(0, -1);
|
|
34
|
+
|
|
35
|
+
const cells: string[] = [];
|
|
36
|
+
let cell = '';
|
|
37
|
+
for (let i = 0; i < value.length; i++) {
|
|
38
|
+
const char = value[i];
|
|
39
|
+
if (char === '\\' && value[i + 1] === '|') {
|
|
40
|
+
cell += '|';
|
|
41
|
+
i++;
|
|
42
|
+
} else if (char === '|') {
|
|
43
|
+
cells.push(cell.trim());
|
|
44
|
+
cell = '';
|
|
45
|
+
} else {
|
|
46
|
+
cell += char;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
cells.push(cell.trim());
|
|
50
|
+
return cells;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A small CSV-row parser so quoted commas remain inside their cell. */
|
|
54
|
+
function splitCommaLine(line: string): string[] {
|
|
55
|
+
const cells: string[] = [];
|
|
56
|
+
let cell = '';
|
|
57
|
+
let quoted = false;
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < line.length; i++) {
|
|
60
|
+
const char = line[i];
|
|
61
|
+
if (char === '"' && !quoted && cell.trim().length === 0) {
|
|
62
|
+
// A quote only opens CSV quoting at the start of a field. Literal
|
|
63
|
+
// quotes elsewhere (for example 5" screen) remain part of the value.
|
|
64
|
+
quoted = true;
|
|
65
|
+
cell = '';
|
|
66
|
+
} else if (char === '"' && quoted) {
|
|
67
|
+
if (line[i + 1] === '"') {
|
|
68
|
+
cell += '"';
|
|
69
|
+
i++;
|
|
70
|
+
} else {
|
|
71
|
+
quoted = false;
|
|
72
|
+
}
|
|
73
|
+
} else if (char === ',' && !quoted) {
|
|
74
|
+
cells.push(cell.trim());
|
|
75
|
+
cell = '';
|
|
76
|
+
} else {
|
|
77
|
+
cell += char;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (quoted) return line.split(',').map((value) => value.trim());
|
|
81
|
+
cells.push(cell.trim());
|
|
82
|
+
return cells;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function splitLine(line: string, delimiter: CandidateDelimiter): string[] {
|
|
86
|
+
switch (delimiter) {
|
|
87
|
+
case 'pipe':
|
|
88
|
+
return splitPipeLine(line);
|
|
89
|
+
case 'comma':
|
|
90
|
+
return splitCommaLine(line);
|
|
91
|
+
case 'tab':
|
|
92
|
+
return line
|
|
93
|
+
.trim()
|
|
94
|
+
.split('\t')
|
|
95
|
+
.map((cell) => cell.trim());
|
|
96
|
+
case 'multispace':
|
|
97
|
+
return line
|
|
98
|
+
.trim()
|
|
99
|
+
.split(/[ \u00a0]{2,}/)
|
|
100
|
+
.map((cell) => cell.trim());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Detect a delimiter only when at least two lines use it and every line
|
|
106
|
+
* produces the same number of columns. Inconsistent prose therefore falls
|
|
107
|
+
* back to a useful one-column table instead of losing text.
|
|
108
|
+
*/
|
|
109
|
+
export function selectionToTable(text: string): SelectionTable {
|
|
110
|
+
const lines = selectionLines(text);
|
|
111
|
+
const candidates: CandidateDelimiter[] = ['pipe', 'comma', 'tab', 'multispace'];
|
|
112
|
+
|
|
113
|
+
if (lines.length >= 2) {
|
|
114
|
+
for (const delimiter of candidates) {
|
|
115
|
+
const rows = lines.map((line) => splitLine(line, delimiter));
|
|
116
|
+
const columnCount = rows[0]?.length ?? 0;
|
|
117
|
+
if (columnCount >= 2 && rows.every((row) => row.length === columnCount)) {
|
|
118
|
+
return { delimiter, rows };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { delimiter: null, rows: lines.map((line) => [line]) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function escapeTableCell(cell: string): string {
|
|
127
|
+
return cell.replace(/\|/g, '\\|');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Render a selection as a GFM table, using its first row as the header. */
|
|
131
|
+
export function selectionToTableMarkdown(text: string): string {
|
|
132
|
+
const { rows } = selectionToTable(text);
|
|
133
|
+
if (rows.length === 0) return '';
|
|
134
|
+
|
|
135
|
+
const formatRow = (row: string[]) => `| ${row.map(escapeTableCell).join(' | ')} |`;
|
|
136
|
+
const separator = rows[0].map(() => '---');
|
|
137
|
+
return [formatRow(rows[0]), formatRow(separator), ...rows.slice(1).map(formatRow)].join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Convert selected lines to task items, normalizing existing list markers. */
|
|
141
|
+
export function selectionToTaskItems(text: string): SelectionTaskItem[] {
|
|
142
|
+
return selectionLines(text).map((line) => {
|
|
143
|
+
const task = /^[-*+]\s+\[([ xX])\]\s*(.*)$/.exec(line);
|
|
144
|
+
if (task) return { checked: task[1].toLowerCase() === 'x', text: task[2].trim() };
|
|
145
|
+
|
|
146
|
+
const withoutListMarker = line.replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, '');
|
|
147
|
+
return { checked: false, text: withoutListMarker.trim() };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function selectionToTaskListMarkdown(text: string): string {
|
|
152
|
+
return selectionToTaskItems(text)
|
|
153
|
+
.map((item) => `- [${item.checked ? 'x' : ' '}] ${item.text}`)
|
|
154
|
+
.join('\n');
|
|
155
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* ASCII diagram fences in the WYSIWYG editor.
|
|
2
|
+
*
|
|
3
|
+
* A qualifying code fence is hidden behind its interactive canvas
|
|
4
|
+
* (AsciiDiagramWidget); toggling "Source" on the widget swaps the hide
|
|
5
|
+
* class for a monospace, non-wrapping view of the raw art. */
|
|
6
|
+
|
|
7
|
+
.squisq-wysiwyg-editor pre.squisq-ascii-fence-hidden {
|
|
8
|
+
display: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.squisq-wysiwyg-editor pre.squisq-ascii-fence-source {
|
|
12
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
13
|
+
/* prosemirror.css sets `.ProseMirror pre { white-space: pre-wrap }`,
|
|
14
|
+
which soft-wraps and visually scrambles column-aligned art. */
|
|
15
|
+
white-space: pre;
|
|
16
|
+
overflow-x: auto;
|
|
17
|
+
line-height: 1.2;
|
|
18
|
+
tab-size: 4;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.squisq-ascii-diagram-widget-host {
|
|
22
|
+
margin: 0.75rem 0 1.25rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.squisq-ascii-diagram-warnings {
|
|
26
|
+
margin-top: 4px;
|
|
27
|
+
font-size: 12px;
|
|
28
|
+
color: var(--squisq-theme-text-muted, #6b7280);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.squisq-ascii-diagram-warnings summary {
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
user-select: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.squisq-ascii-diagram-warnings ul {
|
|
37
|
+
margin: 4px 0 0;
|
|
38
|
+
padding-left: 18px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* "Repair as diagram" affordance on broken box-art fences. The fence stays
|
|
42
|
+
* visible above this banner; clicking rewrites it to clean diagram art. */
|
|
43
|
+
.squisq-repair-banner {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
margin: 4px 0 1rem;
|
|
49
|
+
padding: 8px 12px;
|
|
50
|
+
border: 1px dashed var(--squisq-border, #d1d5db);
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
background: var(--squisq-input-bg, #f9fafb);
|
|
53
|
+
font-size: 13px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.squisq-repair-banner-text {
|
|
57
|
+
color: var(--squisq-text-muted, #6b7280);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.squisq-repair-banner-btn {
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
border: 1px solid var(--squisq-accent, #6366f1);
|
|
63
|
+
border-radius: 6px;
|
|
64
|
+
background: var(--squisq-accent, #6366f1);
|
|
65
|
+
color: #fff;
|
|
66
|
+
padding: 4px 12px;
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
line-height: 1.4;
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.squisq-repair-banner-btn:hover {
|
|
74
|
+
filter: brightness(1.05);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.squisq-repair-banner-btn:active {
|
|
78
|
+
filter: brightness(0.95);
|
|
79
|
+
}
|