@bendyline/squisq-editor-react 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +40 -0
- package/README.md +10 -2
- package/dist/index.d.ts +683 -60
- package/dist/index.js +9648 -3750
- package/dist/monaco.js +0 -1
- package/dist/styles/index.css +1160 -241
- package/package.json +10 -6
- package/dist/index.js.map +0 -1
- package/dist/monaco.js.map +0 -1
- package/src/BlockCardView.tsx +0 -121
- package/src/BlockPreviewPanel.tsx +0 -69
- package/src/BlockPropertiesPopover.tsx +0 -222
- package/src/DocumentSettingsDialog.tsx +0 -272
- package/src/DropZoneOverlay.tsx +0 -137
- package/src/EditorContext.tsx +0 -1247
- package/src/EditorShell.tsx +0 -1334
- package/src/EmojiPicker.tsx +0 -332
- package/src/FolderView.tsx +0 -131
- package/src/Icon.tsx +0 -26
- package/src/ImageEditor.tsx +0 -395
- package/src/ImageNodeView.tsx +0 -284
- package/src/ImageViewer.tsx +0 -221
- package/src/InlineIcon.ts +0 -84
- package/src/InlinePreviewGutter.tsx +0 -590
- package/src/LinkDialog.tsx +0 -276
- package/src/MediaBin.tsx +0 -500
- package/src/MentionExtension.tsx +0 -261
- package/src/OutlinePanel.tsx +0 -547
- package/src/PlainHtmlPreview.tsx +0 -277
- package/src/PreviewControls.tsx +0 -1250
- package/src/PreviewPanel.tsx +0 -422
- package/src/RawEditor.tsx +0 -731
- package/src/RecorderEntry.tsx +0 -159
- package/src/StatusBar.tsx +0 -51
- package/src/TemplateAnnotation.ts +0 -167
- package/src/TemplateContentPreview.tsx +0 -56
- package/src/TemplatePicker.tsx +0 -1314
- package/src/ThemeCustomizerPanel.tsx +0 -304
- package/src/ThemePicker.tsx +0 -433
- package/src/TimelineBlockPreview.tsx +0 -37
- package/src/TimelineTrack.tsx +0 -671
- package/src/Toolbar.tsx +0 -2587
- package/src/Tooltip.tsx +0 -142
- package/src/TransitionPicker.tsx +0 -390
- package/src/VersionHistoryPanel.tsx +0 -359
- package/src/ViewMenuPanel.tsx +0 -189
- package/src/ViewSwitcher.tsx +0 -56
- package/src/WysiwygEditor.tsx +0 -792
- package/src/__tests__/blockProperties.test.ts +0 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +0 -33
- package/src/__tests__/blockRange.test.ts +0 -105
- package/src/__tests__/blockTagActivity.test.ts +0 -183
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +0 -75
- package/src/__tests__/buildPreviewDocContent.test.ts +0 -48
- package/src/__tests__/buildPreviewDocTransition.test.ts +0 -92
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +0 -60
- package/src/__tests__/codeContextSectionView.test.tsx +0 -97
- package/src/__tests__/codeContextZoneManager.test.ts +0 -127
- package/src/__tests__/createShapeLayer.test.ts +0 -46
- package/src/__tests__/customLayoutManagerTheme.test.tsx +0 -34
- package/src/__tests__/detectMarkdown.test.ts +0 -73
- package/src/__tests__/diffContextSections.test.ts +0 -39
- package/src/__tests__/documentSettingsDialog.test.tsx +0 -197
- package/src/__tests__/drawingShapeRoundTrip.test.ts +0 -49
- package/src/__tests__/editorScrollHandoff.test.tsx +0 -102
- package/src/__tests__/editorScrollSync.test.ts +0 -65
- package/src/__tests__/editorShellCodeContext.test.tsx +0 -86
- package/src/__tests__/editorShellProps.test.tsx +0 -485
- package/src/__tests__/embeddedMedia.test.ts +0 -48
- package/src/__tests__/emojiPicker.test.tsx +0 -93
- package/src/__tests__/fileKind.test.ts +0 -112
- package/src/__tests__/headingTransition.test.ts +0 -176
- package/src/__tests__/imageEditAffordance.test.tsx +0 -272
- package/src/__tests__/imageEditModalContract.test.tsx +0 -119
- package/src/__tests__/imageEditorShell.test.tsx +0 -94
- package/src/__tests__/imageEditorState.test.ts +0 -171
- package/src/__tests__/inlinePreviewGutter.test.tsx +0 -62
- package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +0 -103
- package/src/__tests__/jsonEditor.test.tsx +0 -223
- package/src/__tests__/layersPanel.test.tsx +0 -167
- package/src/__tests__/layoutChildRoundTrip.test.ts +0 -71
- package/src/__tests__/linkDialogDocPicker.test.tsx +0 -137
- package/src/__tests__/majorApiTypes.test.ts +0 -26
- package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
- package/src/__tests__/mediaBinDrop.test.tsx +0 -90
- package/src/__tests__/mediaReferences.test.ts +0 -82
- package/src/__tests__/narrationSave.test.ts +0 -148
- package/src/__tests__/outlinePanel.test.tsx +0 -169
- package/src/__tests__/outlineSource.test.ts +0 -217
- package/src/__tests__/plainHtmlPreview.test.tsx +0 -125
- package/src/__tests__/presentationMode.test.tsx +0 -290
- package/src/__tests__/previewControls.test.tsx +0 -540
- package/src/__tests__/previewPanelAudioFailure.test.tsx +0 -46
- package/src/__tests__/previewPanelPresentation.test.tsx +0 -126
- package/src/__tests__/previewPanelTransforms.test.tsx +0 -117
- package/src/__tests__/propertiesPanel.test.tsx +0 -69
- package/src/__tests__/rawEditorModelIsolation.test.ts +0 -19
- package/src/__tests__/recorderFormats.test.ts +0 -146
- package/src/__tests__/recorderMediaInsert.test.ts +0 -86
- package/src/__tests__/recorderTheme.test.tsx +0 -42
- package/src/__tests__/recorderTimingJson.test.ts +0 -41
- package/src/__tests__/resolveBlockVisual.test.ts +0 -43
- package/src/__tests__/selectionConversions.test.ts +0 -80
- package/src/__tests__/statusBar.test.tsx +0 -27
- package/src/__tests__/teleprompter.test.tsx +0 -190
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +0 -99
- package/src/__tests__/templateContentPreview.test.ts +0 -101
- package/src/__tests__/templatePickerMetadata.test.ts +0 -32
- package/src/__tests__/templatePickerPortal.test.tsx +0 -62
- package/src/__tests__/timelineSource.test.ts +0 -134
- package/src/__tests__/tiptapBridge.test.ts +0 -566
- package/src/__tests__/tiptapBridgeConformance.test.ts +0 -47
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +0 -109
- package/src/__tests__/tiptapImageRoundTrip.test.ts +0 -73
- package/src/__tests__/toolbarSelectionConversion.test.tsx +0 -164
- package/src/__tests__/tooltip.test.tsx +0 -72
- package/src/__tests__/transformStyleId.test.ts +0 -13
- package/src/__tests__/transitionCatalog.test.ts +0 -64
- package/src/__tests__/useBlockNavigator.test.tsx +0 -67
- package/src/__tests__/useImageEditor.test.tsx +0 -226
- package/src/__tests__/useJsonEditorTokens.test.ts +0 -59
- package/src/__tests__/useMediaRecorder.test.ts +0 -302
- package/src/__tests__/useTimelineClock.test.ts +0 -21
- package/src/__tests__/versionHistory.test.tsx +0 -197
- package/src/__tests__/viewMenuPanel.test.tsx +0 -130
- package/src/__tests__/wysiwygImageUpload.test.ts +0 -69
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +0 -56
- package/src/asciiDiagram/AsciiDiagramExtension.ts +0 -343
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +0 -254
- package/src/asciiDiagram/RepairableDiagramExtension.ts +0 -200
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +0 -145
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +0 -159
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +0 -284
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +0 -114
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +0 -43
- package/src/asciiDiagram/asciiDiagramCommands.ts +0 -174
- package/src/asciiDiagram/asciiDiagramData.ts +0 -149
- package/src/asciiDiagram/asciiDiagramOps.ts +0 -238
- package/src/asciiDiagram/asciiPaste.ts +0 -19
- package/src/blockProperties.ts +0 -88
- package/src/blockRange.ts +0 -132
- package/src/blockSlice.ts +0 -75
- package/src/blockTagActivity.ts +0 -233
- package/src/buildDocumentPreviewMarkdown.ts +0 -168
- package/src/buildPreviewDoc.ts +0 -405
- package/src/codeContext/CodeContextSectionView.tsx +0 -124
- package/src/codeContext/CodeContextZoneManager.ts +0 -149
- package/src/codeContext/CodeContextZones.tsx +0 -121
- package/src/codeContext/diffContextSections.ts +0 -38
- package/src/codeContext/types.ts +0 -75
- package/src/customTemplates/AddBin.tsx +0 -126
- package/src/customTemplates/CustomLayoutManager.tsx +0 -234
- package/src/customTemplates/CustomTemplateContext.tsx +0 -188
- package/src/customTemplates/LayerToolbar.tsx +0 -580
- package/src/customTemplates/ShapeGlyph.tsx +0 -47
- package/src/customTemplates/TemplateDesigner.tsx +0 -433
- package/src/customTemplates/__tests__/library.test.ts +0 -96
- package/src/customTemplates/__tests__/normalizePositions.test.ts +0 -109
- package/src/customTemplates/__tests__/shapeDefs.test.ts +0 -49
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +0 -108
- package/src/customTemplates/designer.css +0 -732
- package/src/customTemplates/index.ts +0 -31
- package/src/customTemplates/library.ts +0 -110
- package/src/customTemplates/normalizePositions.ts +0 -75
- package/src/customTemplates/shapeDefs.ts +0 -131
- package/src/customTemplates/thumbnail.tsx +0 -63
- package/src/customTemplates/tokenDefs.ts +0 -60
- package/src/customTemplates/useDocCustomTemplates.ts +0 -52
- package/src/customTemplates/useMemoryLayerAdapter.ts +0 -129
- package/src/customThemes/CustomThemeContext.tsx +0 -185
- package/src/customThemes/CustomThemeDialog.tsx +0 -316
- package/src/customThemes/ImportThemeSection.tsx +0 -178
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +0 -64
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +0 -47
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +0 -60
- package/src/customThemes/__tests__/importThemeSection.test.tsx +0 -192
- package/src/customThemes/customThemeLibrary.ts +0 -100
- package/src/customThemes/index.ts +0 -37
- package/src/customThemes/themeControls.tsx +0 -229
- package/src/customThemes/themeDraft.ts +0 -288
- package/src/customThemes/useDocCustomThemes.ts +0 -49
- package/src/detectMarkdown.ts +0 -62
- package/src/diagram/DiagramCanvas.tsx +0 -259
- package/src/diagram/DiagramMaximizedOverlay.tsx +0 -46
- package/src/diagram/diagramCommands.ts +0 -604
- package/src/diagram/diagramConstants.ts +0 -17
- package/src/diagram/types.ts +0 -35
- package/src/editorScrollSync.ts +0 -73
- package/src/embeddedMedia.ts +0 -78
- package/src/emojiData.ts +0 -1317
- package/src/fileKind.ts +0 -158
- package/src/frontmatter.ts +0 -29
- package/src/frontmatterSettings.ts +0 -23
- package/src/headingTransition.ts +0 -291
- package/src/hooks/useFileDrop.ts +0 -266
- package/src/imageEditor/CanvasSurface.tsx +0 -703
- package/src/imageEditor/ImageVersionHistoryDropdown.tsx +0 -396
- package/src/imageEditor/LayersPanel.tsx +0 -218
- package/src/imageEditor/PropertiesPanel.tsx +0 -474
- package/src/imageEditor/Toolbar.tsx +0 -459
- package/src/imageEditor/createShapeLayer.ts +0 -280
- package/src/imageEditor/icons.tsx +0 -68
- package/src/imageEditor/image-editor.css +0 -574
- package/src/imageEditor/layers/EditorImageLayer.tsx +0 -45
- package/src/imageEditor/layers/EditorShapeLayer.tsx +0 -62
- package/src/imageEditor/layers/EditorTextLayer.tsx +0 -45
- package/src/imageEditor/layers/SelectionHandles.tsx +0 -86
- package/src/imageEditor/state.ts +0 -173
- package/src/imageEditor/useImageEditor.ts +0 -385
- package/src/imageEditor/useImageEditorTokens.ts +0 -70
- package/src/index.ts +0 -470
- package/src/jsonEditor/EmbeddedRichTextField.tsx +0 -81
- package/src/jsonEditor/JsonEditor.tsx +0 -81
- package/src/jsonEditor/JsonEditorContext.tsx +0 -81
- package/src/jsonEditor/RenderNode.tsx +0 -87
- package/src/jsonEditor/editors.tsx +0 -745
- package/src/jsonEditor/index.ts +0 -2
- package/src/jsonEditor/json-editor.css +0 -463
- package/src/jsonEditor/useJsonEditorTokens.ts +0 -33
- package/src/mediaDragMime.ts +0 -32
- package/src/mediaEntries.ts +0 -12
- package/src/mediaReferences.ts +0 -299
- package/src/monaco.ts +0 -35
- package/src/monacoWorkers.ts +0 -89
- package/src/outlineSource.ts +0 -171
- package/src/presentation/PresentationMode.tsx +0 -596
- package/src/rawEditorIsolation.ts +0 -18
- package/src/recorder/RecorderButton.tsx +0 -80
- package/src/recorder/RecorderModal.tsx +0 -706
- package/src/recorder/RecorderPanel.tsx +0 -77
- package/src/recorder/formats.ts +0 -159
- package/src/recorder/hooks/useMediaRecorder.ts +0 -436
- package/src/recorder/hooks/useStreamPreview.ts +0 -47
- package/src/recorder/insertMediaBlock.ts +0 -30
- package/src/recorder/sources/cameraStream.ts +0 -32
- package/src/recorder/sources/micStream.ts +0 -25
- package/src/recorder/sources/screenStream.ts +0 -162
- package/src/recorder/timingJson.ts +0 -66
- package/src/resolveBlockVisual.ts +0 -39
- package/src/scene/Scene.tsx +0 -616
- package/src/scene/SceneBlockExtension.ts +0 -205
- package/src/scene/SceneBlockToolbar.tsx +0 -201
- package/src/scene/SceneBlockWidget.tsx +0 -441
- package/src/scene/ScenePropsBar.tsx +0 -85
- package/src/scene/SceneSelection.tsx +0 -111
- package/src/scene/SceneSideToolbar.tsx +0 -89
- package/src/scene/SceneViewport.tsx +0 -106
- package/src/scene/ShapePalette.tsx +0 -196
- package/src/scene/__tests__/DiagramAdapter.test.ts +0 -142
- package/src/scene/__tests__/SceneBlockExtension.test.ts +0 -33
- package/src/scene/__tests__/bezierEdit.test.ts +0 -85
- package/src/scene/__tests__/blockLayers.test.ts +0 -57
- package/src/scene/__tests__/sceneIsolation.test.tsx +0 -119
- package/src/scene/__tests__/shapeLayers.test.ts +0 -106
- package/src/scene/__tests__/useSceneHitTest.test.ts +0 -90
- package/src/scene/__tests__/useScenePanZoom.test.ts +0 -103
- package/src/scene/adapters/DiagramAdapter.ts +0 -79
- package/src/scene/adapters/DrawingAdapter.ts +0 -420
- package/src/scene/adapters/LayoutAdapter.ts +0 -313
- package/src/scene/adapters/blockLayers.ts +0 -159
- package/src/scene/commands/SceneCommand.ts +0 -84
- package/src/scene/commands/drawingCommands.ts +0 -318
- package/src/scene/commands/layoutCommands.ts +0 -301
- package/src/scene/hooks/useSceneHitTest.ts +0 -105
- package/src/scene/hooks/useScenePanZoom.ts +0 -147
- package/src/scene/hooks/useSceneSelection.ts +0 -62
- package/src/scene/index.ts +0 -90
- package/src/scene/layers/DiagramEdges.tsx +0 -142
- package/src/scene/layers/edgeGeometry.ts +0 -104
- package/src/scene/layers/nodeCard.tsx +0 -164
- package/src/scene/layers/renderLayer.tsx +0 -70
- package/src/scene/layers/shapeLayers.ts +0 -201
- package/src/scene/paths/bezierEdit.ts +0 -208
- package/src/scene/scene.css +0 -792
- package/src/scene/text/SceneTextOverlay.tsx +0 -161
- package/src/scene/text/sceneTextChannel.ts +0 -46
- package/src/scene/text/sceneTextConfig.ts +0 -31
- package/src/scene/text/sceneTiptap.ts +0 -36
- package/src/scene/text/useSceneTextEditing.ts +0 -39
- package/src/scene/tools/ConnectTool.ts +0 -209
- package/src/scene/tools/DrawingConnectTool.ts +0 -231
- package/src/scene/tools/PathTool.ts +0 -158
- package/src/scene/tools/PlaceTool.ts +0 -47
- package/src/scene/tools/SceneTool.ts +0 -85
- package/src/scene/tools/SelectTool.ts +0 -300
- package/src/scene/tools/ShapeTool.ts +0 -143
- package/src/scene/tools/TextTool.ts +0 -72
- package/src/scene/tools/TokenTool.ts +0 -95
- package/src/scene/tools/createDrawShapeTool.ts +0 -82
- package/src/selectionConversions.ts +0 -155
- package/src/styles/ascii-diagram.css +0 -79
- package/src/styles/ascii-timeline.css +0 -596
- package/src/styles/code-context.css +0 -155
- package/src/styles/diagram.css +0 -183
- package/src/styles/editor.css +0 -6811
- package/src/styles/folder-view.css +0 -210
- package/src/styles/image-edit-affordance.css +0 -201
- package/src/styles/index.css +0 -18
- package/src/styles/tree-view.css +0 -189
- package/src/teleprompter/TeleprompterControls.tsx +0 -218
- package/src/teleprompter/TeleprompterSelfView.tsx +0 -22
- package/src/teleprompter/TeleprompterSurface.tsx +0 -267
- package/src/teleprompter/TeleprompterView.tsx +0 -338
- package/src/teleprompter/canvasRenderer.ts +0 -161
- package/src/teleprompter/floatingWindow.ts +0 -352
- package/src/teleprompter/index.ts +0 -61
- package/src/teleprompter/pcmWorklet.ts +0 -62
- package/src/teleprompter/recording/insertPreamble.ts +0 -80
- package/src/teleprompter/recording/narrationSave.ts +0 -134
- package/src/teleprompter/recording/useNarrationRecorder.ts +0 -367
- package/src/teleprompter/scrollModel.ts +0 -85
- package/src/teleprompter/teleprompterTheme.ts +0 -303
- package/src/teleprompter/types.ts +0 -38
- package/src/teleprompter/useFloatingWindow.ts +0 -74
- package/src/teleprompter/useMicAnalysis.ts +0 -211
- package/src/teleprompter/useTeleprompter.ts +0 -421
- package/src/templateContentPreviewResolver.ts +0 -360
- package/src/timeline/TimelineEditorWidget.tsx +0 -861
- package/src/timeline/TimelineViewExtension.ts +0 -252
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +0 -414
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +0 -163
- package/src/timeline/__tests__/timelineCommands.test.ts +0 -359
- package/src/timeline/__tests__/timelineOps.test.ts +0 -277
- package/src/timeline/__tests__/timelinePaste.test.ts +0 -34
- package/src/timeline/timelineCommands.ts +0 -421
- package/src/timeline/timelineData.ts +0 -54
- package/src/timeline/timelineOps.ts +0 -381
- package/src/timeline/timelinePaste.ts +0 -8
- package/src/timelineSource.ts +0 -244
- package/src/tiptap/TiptapAudio.tsx +0 -86
- package/src/tiptap/TiptapVideo.tsx +0 -119
- package/src/tiptap/useResolvedMediaSrc.ts +0 -47
- package/src/tiptapBridge.ts +0 -986
- package/src/tooltipPlacement.ts +0 -13
- package/src/transformStyleId.ts +0 -17
- package/src/transitionCatalog.ts +0 -159
- package/src/treeview/TreeOutlineWidget.tsx +0 -390
- package/src/treeview/TreeViewExtension.ts +0 -250
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +0 -156
- package/src/treeview/__tests__/TreeViewExtension.test.ts +0 -122
- package/src/treeview/__tests__/treeOps.test.ts +0 -177
- package/src/treeview/__tests__/treePaste.test.ts +0 -40
- package/src/treeview/__tests__/treeViewCommands.test.ts +0 -179
- package/src/treeview/treeOps.ts +0 -231
- package/src/treeview/treePaste.ts +0 -13
- package/src/treeview/treeViewCommands.ts +0 -99
- package/src/treeview/treeViewData.ts +0 -41
- package/src/types/monaco-shims.d.ts +0 -10
- package/src/useBlockNavigator.ts +0 -153
- package/src/useHeadingLayout.ts +0 -294
- package/src/useMonacoLoader.ts +0 -82
- package/src/useTimelineClock.ts +0 -76
- package/src/utils/collectInlineFontAwesomeCss.ts +0 -69
- package/src/utils/dropUtils.ts +0 -136
- package/src/utils/normalizeMalformedAssetUrl.ts +0 -22
- package/src/wysiwygImageUpload.ts +0 -113
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vitest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
6
|
-
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
7
|
-
import {
|
|
8
|
-
buildNarrationScript,
|
|
9
|
-
parseNarrationTimingJson,
|
|
10
|
-
type NarrationAlignment,
|
|
11
|
-
} from '@bendyline/squisq/narration';
|
|
12
|
-
import {
|
|
13
|
-
insertNarrationPreamble,
|
|
14
|
-
narrationAnnotationLine,
|
|
15
|
-
} from '../teleprompter/recording/insertPreamble';
|
|
16
|
-
import { buildNarrationSavePlan } from '../teleprompter/recording/narrationSave';
|
|
17
|
-
|
|
18
|
-
describe('insertNarrationPreamble', () => {
|
|
19
|
-
it('inserts at the top of a plain document', () => {
|
|
20
|
-
const next = insertNarrationPreamble('# Title\n\nBody.\n', 'audio/narration-1.webm', null);
|
|
21
|
-
expect(next.startsWith('{[audio src=audio/narration-1.webm anchor=document]}\n\n# Title')).toBe(
|
|
22
|
-
true,
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('inserts after the frontmatter fence', () => {
|
|
27
|
-
const source = '---\ntitle: Doc\n---\n\n# Title\n\nBody.\n';
|
|
28
|
-
const next = insertNarrationPreamble(source, 'audio/narration-1.webm', null);
|
|
29
|
-
const lines = next.split('\n');
|
|
30
|
-
expect(lines[2]).toBe('---');
|
|
31
|
-
expect(next).toContain(
|
|
32
|
-
'---\n\n{[audio src=audio/narration-1.webm anchor=document]}\n\n# Title',
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('replaces a previous take (and its camera line) on retake', () => {
|
|
37
|
-
const withTake = insertNarrationPreamble(
|
|
38
|
-
'# Title\n\nBody.\n',
|
|
39
|
-
'audio/narration-old.webm',
|
|
40
|
-
'video/narration-cam-old.webm',
|
|
41
|
-
);
|
|
42
|
-
expect(withTake).toContain('narration-old');
|
|
43
|
-
expect(withTake).toContain('<video src="video/narration-cam-old.webm"');
|
|
44
|
-
|
|
45
|
-
const retaken = insertNarrationPreamble(withTake, 'audio/narration-new.webm', null);
|
|
46
|
-
expect(retaken).not.toContain('narration-old');
|
|
47
|
-
expect(retaken).not.toContain('narration-cam-old');
|
|
48
|
-
expect(retaken).toContain('{[audio src=audio/narration-new.webm anchor=document]}');
|
|
49
|
-
// No duplicate annotations.
|
|
50
|
-
expect(retaken.match(/\{\[audio /g)?.length).toBe(1);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('adds the camera companion as an inline video line', () => {
|
|
54
|
-
const next = insertNarrationPreamble(
|
|
55
|
-
'# Title\n\nBody.\n',
|
|
56
|
-
'audio/narration-1.webm',
|
|
57
|
-
'video/narration-cam-1.webm',
|
|
58
|
-
);
|
|
59
|
-
expect(next).toContain('{[audio src=audio/narration-1.webm anchor=document]}');
|
|
60
|
-
expect(next).toContain('<video src="video/narration-cam-1.webm" controls width="240"></video>');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('quotes srcs with spaces', () => {
|
|
64
|
-
expect(narrationAnnotationLine('audio/my take.webm')).toBe(
|
|
65
|
-
'{[audio src="audio/my take.webm" anchor=document]}',
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('round-trips through markdownToDoc as documentMedia', () => {
|
|
70
|
-
const next = insertNarrationPreamble(
|
|
71
|
-
'---\ntitle: Doc\n---\n\n# Title\n\nBody.\n',
|
|
72
|
-
'audio/narration-1.webm',
|
|
73
|
-
null,
|
|
74
|
-
);
|
|
75
|
-
const doc = markdownToDoc(parseMarkdown(next));
|
|
76
|
-
expect(doc.documentMedia?.length).toBe(1);
|
|
77
|
-
expect(doc.documentMedia![0].src).toBe('audio/narration-1.webm');
|
|
78
|
-
expect(doc.documentMedia![0].anchor).toBe('document');
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('buildNarrationSavePlan', () => {
|
|
83
|
-
const script = buildNarrationScript(
|
|
84
|
-
markdownToDoc(parseMarkdown('# One\n\nAlpha beta.\n\n# Two\n\nGamma delta.\n')),
|
|
85
|
-
);
|
|
86
|
-
const alignment: NarrationAlignment = {
|
|
87
|
-
words: script.tokens.map((_, i) => ({ tokenIndex: i, tSec: i * 0.4, interpolated: false })),
|
|
88
|
-
blocks: script.blocks.map((range, i) => ({
|
|
89
|
-
blockId: range.blockId,
|
|
90
|
-
blockIndex: i,
|
|
91
|
-
charStart: range.charStart,
|
|
92
|
-
charEnd: range.charEnd,
|
|
93
|
-
startSec: i * 5,
|
|
94
|
-
endSec: (i + 1) * 5,
|
|
95
|
-
})),
|
|
96
|
-
detectedSyllables: script.totalSyllables,
|
|
97
|
-
cost: 0,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
it('produces recorder-convention paths and a v3 sidecar the parser accepts', () => {
|
|
101
|
-
const plan = buildNarrationSavePlan({
|
|
102
|
-
script,
|
|
103
|
-
alignment,
|
|
104
|
-
durationSec: 10,
|
|
105
|
-
audioExt: '.webm',
|
|
106
|
-
cameraExt: '.webm',
|
|
107
|
-
baseWpm: 150,
|
|
108
|
-
cameraOffsetSec: 0.02,
|
|
109
|
-
});
|
|
110
|
-
expect(plan.audioRelativeName).toMatch(/^audio\/narration-.*\.webm$/);
|
|
111
|
-
expect(plan.cameraRelativeName).toMatch(/^video\/narration-cam.*\.webm$/);
|
|
112
|
-
expect(plan.sidecarPathFor('audio/renamed.webm')).toBe('audio/renamed.webm.timing.json');
|
|
113
|
-
|
|
114
|
-
const parsed = parseNarrationTimingJson(JSON.stringify(plan.sidecarPayload));
|
|
115
|
-
expect(parsed).not.toBeNull();
|
|
116
|
-
expect(parsed!.blocks.length).toBe(script.blocks.length);
|
|
117
|
-
expect(parsed!.bookmarks.length).toBe(script.tokens.length);
|
|
118
|
-
expect(parsed!.cameraOffsetSec).toBeCloseTo(0.02, 6);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('degrades to an empty-timing sidecar when alignment failed', () => {
|
|
122
|
-
const plan = buildNarrationSavePlan({
|
|
123
|
-
script,
|
|
124
|
-
alignment: null,
|
|
125
|
-
durationSec: 10,
|
|
126
|
-
audioExt: '.webm',
|
|
127
|
-
cameraExt: null,
|
|
128
|
-
baseWpm: 150,
|
|
129
|
-
});
|
|
130
|
-
expect(plan.cameraRelativeName).toBeNull();
|
|
131
|
-
expect(plan.sidecarPayload.blocks).toEqual([]);
|
|
132
|
-
expect(plan.sidecarPayload.bookmarks).toEqual([]);
|
|
133
|
-
expect(plan.sidecarPayload.sourceText).toBe(script.sourceText);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('nextMarkdown composes the preamble from one snapshot', () => {
|
|
137
|
-
const plan = buildNarrationSavePlan({
|
|
138
|
-
script,
|
|
139
|
-
alignment,
|
|
140
|
-
durationSec: 10,
|
|
141
|
-
audioExt: '.webm',
|
|
142
|
-
cameraExt: null,
|
|
143
|
-
baseWpm: 150,
|
|
144
|
-
});
|
|
145
|
-
const next = plan.nextMarkdown('# One\n\nAlpha beta.\n', 'audio/narration-x.webm', null);
|
|
146
|
-
expect(next).toContain('{[audio src=audio/narration-x.webm anchor=document]}');
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, beforeAll, vi } from 'vitest';
|
|
2
|
-
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
-
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
4
|
-
import { OutlinePanel } from '../OutlinePanel';
|
|
5
|
-
|
|
6
|
-
// jsdom lacks ResizeObserver — the pane wires one up to track the editor's
|
|
7
|
-
// page edge. A no-op shim is enough for the rendering tests below.
|
|
8
|
-
beforeAll(() => {
|
|
9
|
-
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
10
|
-
globalThis.ResizeObserver = class {
|
|
11
|
-
observe() {}
|
|
12
|
-
unobserve() {}
|
|
13
|
-
disconnect() {}
|
|
14
|
-
} as unknown as typeof ResizeObserver;
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
function renderOutline(markdown: string) {
|
|
19
|
-
return render(
|
|
20
|
-
<EditorProvider initialMarkdown={markdown} initialView="wysiwyg" articleId="test">
|
|
21
|
-
<OutlinePanel />
|
|
22
|
-
</EditorProvider>,
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function SourceProbe() {
|
|
27
|
-
const { markdownSource } = useEditorContext();
|
|
28
|
-
return <output data-testid="markdown-source">{markdownSource}</output>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function renderOutlineWithSource(markdown: string, readOnly = false) {
|
|
32
|
-
return render(
|
|
33
|
-
<EditorProvider initialMarkdown={markdown} initialView="wysiwyg" articleId="test">
|
|
34
|
-
<OutlinePanel readOnly={readOnly} />
|
|
35
|
-
<SourceProbe />
|
|
36
|
-
</EditorProvider>,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function makeDataTransfer(): DataTransfer {
|
|
41
|
-
const values = new Map<string, string>();
|
|
42
|
-
return {
|
|
43
|
-
effectAllowed: 'uninitialized',
|
|
44
|
-
dropEffect: 'none',
|
|
45
|
-
setData: vi.fn((type: string, value: string) => values.set(type, value)),
|
|
46
|
-
getData: vi.fn((type: string) => values.get(type) ?? ''),
|
|
47
|
-
get types() {
|
|
48
|
-
return [...values.keys()];
|
|
49
|
-
},
|
|
50
|
-
} as unknown as DataTransfer;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe('OutlinePanel', () => {
|
|
54
|
-
it('renders the empty placeholder when the doc has no headings', async () => {
|
|
55
|
-
renderOutline('Just a paragraph, no headings.\n');
|
|
56
|
-
expect(await screen.findByText(/add a heading to populate the outline/i)).toBeTruthy();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('renders one row per heading and nests by depth', async () => {
|
|
60
|
-
const md = [
|
|
61
|
-
'# Top Level',
|
|
62
|
-
'',
|
|
63
|
-
'## Subsection A',
|
|
64
|
-
'',
|
|
65
|
-
'Body.',
|
|
66
|
-
'',
|
|
67
|
-
'### Detail',
|
|
68
|
-
'',
|
|
69
|
-
'Body.',
|
|
70
|
-
'',
|
|
71
|
-
'## Subsection B',
|
|
72
|
-
'',
|
|
73
|
-
'Body.',
|
|
74
|
-
'',
|
|
75
|
-
].join('\n');
|
|
76
|
-
|
|
77
|
-
const { container } = renderOutline(md);
|
|
78
|
-
await screen.findByTestId('outline-panel');
|
|
79
|
-
|
|
80
|
-
const rows = container.querySelectorAll('.squisq-outline-row');
|
|
81
|
-
expect(rows.length).toBe(4);
|
|
82
|
-
|
|
83
|
-
// Depth modifier classes are applied per heading level.
|
|
84
|
-
const depths = Array.from(rows).map((r) => {
|
|
85
|
-
const match = r.className.match(/squisq-outline-row--depth-(\d)/);
|
|
86
|
-
return match ? Number(match[1]) : null;
|
|
87
|
-
});
|
|
88
|
-
expect(depths).toEqual([1, 2, 3, 2]);
|
|
89
|
-
|
|
90
|
-
// Heading text is reflected in row labels.
|
|
91
|
-
const labels = Array.from(rows).map((r) => r.textContent?.trim());
|
|
92
|
-
expect(labels).toContain('Top Level');
|
|
93
|
-
expect(labels).toContain('Subsection A');
|
|
94
|
-
expect(labels).toContain('Detail');
|
|
95
|
-
expect(labels).toContain('Subsection B');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('shows a template chip on annotated headings', async () => {
|
|
99
|
-
const md = '# Welcome {[title]}\n\nIntro.\n';
|
|
100
|
-
const { container } = renderOutline(md);
|
|
101
|
-
await screen.findByTestId('outline-panel');
|
|
102
|
-
const chip = container.querySelector('.squisq-outline-template-chip');
|
|
103
|
-
expect(chip).not.toBeNull();
|
|
104
|
-
expect(chip!.textContent).toContain('Title');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('drags a section after a sibling and rewrites the underlying markdown', async () => {
|
|
108
|
-
const source = '# Alpha\n\nalpha\n\n# Bravo\n\nbravo\n';
|
|
109
|
-
const expected = '# Bravo\n\nbravo\n\n# Alpha\n\nalpha\n';
|
|
110
|
-
const { container } = renderOutlineWithSource(source);
|
|
111
|
-
const alpha = await screen.findByRole('button', { name: 'Alpha' });
|
|
112
|
-
const bravo = await screen.findByRole('button', { name: 'Bravo' });
|
|
113
|
-
const bravoWrap = bravo.closest<HTMLElement>('.squisq-outline-row-wrap');
|
|
114
|
-
expect(bravoWrap).not.toBeNull();
|
|
115
|
-
vi.spyOn(bravoWrap!, 'getBoundingClientRect').mockReturnValue({
|
|
116
|
-
x: 0,
|
|
117
|
-
y: 0,
|
|
118
|
-
top: 0,
|
|
119
|
-
right: 200,
|
|
120
|
-
bottom: 20,
|
|
121
|
-
left: 0,
|
|
122
|
-
width: 200,
|
|
123
|
-
height: 20,
|
|
124
|
-
toJSON: () => ({}),
|
|
125
|
-
});
|
|
126
|
-
const transfer = makeDataTransfer();
|
|
127
|
-
|
|
128
|
-
fireEvent.dragStart(alpha, { dataTransfer: transfer });
|
|
129
|
-
expect(transfer.effectAllowed).toBe('move');
|
|
130
|
-
expect(transfer.setData).toHaveBeenCalledWith('text/plain', expect.any(String));
|
|
131
|
-
|
|
132
|
-
fireEvent.dragOver(bravoWrap!, { dataTransfer: transfer, clientY: 18 });
|
|
133
|
-
expect(transfer.dropEffect).toBe('move');
|
|
134
|
-
expect(bravo.closest('.squisq-outline-item')?.classList).toContain(
|
|
135
|
-
'squisq-outline-item--drop-after',
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
fireEvent.drop(bravoWrap!, { dataTransfer: transfer, clientY: 18 });
|
|
139
|
-
expect(container.querySelector('.squisq-outline-item--drop-after')).toBeNull();
|
|
140
|
-
|
|
141
|
-
await waitFor(() => {
|
|
142
|
-
expect(screen.getByTestId('markdown-source').textContent).toBe(expected);
|
|
143
|
-
const labels = Array.from(container.querySelectorAll('.squisq-outline-row-text')).map(
|
|
144
|
-
(row) => row.textContent,
|
|
145
|
-
);
|
|
146
|
-
expect(labels).toEqual(['Bravo', 'Alpha']);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('disables outline mutations in read-only mode', async () => {
|
|
151
|
-
const source = '# Alpha\n\nalpha\n\n# Bravo\n\nbravo\n';
|
|
152
|
-
renderOutlineWithSource(source, true);
|
|
153
|
-
const alpha = await screen.findByRole('button', { name: 'Alpha' });
|
|
154
|
-
const bravo = await screen.findByRole('button', { name: 'Bravo' });
|
|
155
|
-
const bravoWrap = bravo.closest<HTMLElement>('.squisq-outline-row-wrap');
|
|
156
|
-
const transfer = makeDataTransfer();
|
|
157
|
-
|
|
158
|
-
expect(alpha.getAttribute('draggable')).toBe('false');
|
|
159
|
-
expect(
|
|
160
|
-
screen
|
|
161
|
-
.getAllByRole('button', { name: /demote heading \(currently h1\)/i })[0]
|
|
162
|
-
.hasAttribute('disabled'),
|
|
163
|
-
).toBe(true);
|
|
164
|
-
fireEvent.dragStart(alpha, { dataTransfer: transfer });
|
|
165
|
-
fireEvent.drop(bravoWrap!, { dataTransfer: transfer, clientY: 100 });
|
|
166
|
-
|
|
167
|
-
expect(screen.getByTestId('markdown-source').textContent).toBe(source);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { moveHeadingSectionInSource } from '../outlineSource';
|
|
3
|
-
|
|
4
|
-
function lineOf(source: string, heading: string): number {
|
|
5
|
-
const index = source.indexOf(heading);
|
|
6
|
-
if (index < 0) throw new Error(`Missing heading: ${heading}`);
|
|
7
|
-
return source.slice(0, index).split(/\r\n|\r|\n/).length;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe('moveHeadingSectionInSource', () => {
|
|
11
|
-
it('moves a parent section with its body and nested subsections', () => {
|
|
12
|
-
const source = [
|
|
13
|
-
'# Document',
|
|
14
|
-
'',
|
|
15
|
-
'Intro.',
|
|
16
|
-
'',
|
|
17
|
-
'## First',
|
|
18
|
-
'',
|
|
19
|
-
'First body.',
|
|
20
|
-
'',
|
|
21
|
-
'### First child',
|
|
22
|
-
'',
|
|
23
|
-
'Child body.',
|
|
24
|
-
'',
|
|
25
|
-
'## Second',
|
|
26
|
-
'',
|
|
27
|
-
'Second body.',
|
|
28
|
-
'',
|
|
29
|
-
'### Second child',
|
|
30
|
-
'',
|
|
31
|
-
'Second child body.',
|
|
32
|
-
'',
|
|
33
|
-
'## Third',
|
|
34
|
-
'',
|
|
35
|
-
'Third body.',
|
|
36
|
-
'',
|
|
37
|
-
].join('\n');
|
|
38
|
-
|
|
39
|
-
const next = moveHeadingSectionInSource(
|
|
40
|
-
source,
|
|
41
|
-
lineOf(source, '## First'),
|
|
42
|
-
lineOf(source, '## Second'),
|
|
43
|
-
'after',
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
expect(next).toBe(
|
|
47
|
-
[
|
|
48
|
-
'# Document',
|
|
49
|
-
'',
|
|
50
|
-
'Intro.',
|
|
51
|
-
'',
|
|
52
|
-
'## Second',
|
|
53
|
-
'',
|
|
54
|
-
'Second body.',
|
|
55
|
-
'',
|
|
56
|
-
'### Second child',
|
|
57
|
-
'',
|
|
58
|
-
'Second child body.',
|
|
59
|
-
'',
|
|
60
|
-
'## First',
|
|
61
|
-
'',
|
|
62
|
-
'First body.',
|
|
63
|
-
'',
|
|
64
|
-
'### First child',
|
|
65
|
-
'',
|
|
66
|
-
'Child body.',
|
|
67
|
-
'',
|
|
68
|
-
'## Third',
|
|
69
|
-
'',
|
|
70
|
-
'Third body.',
|
|
71
|
-
'',
|
|
72
|
-
].join('\n'),
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('moves a later section before an earlier sibling', () => {
|
|
77
|
-
const source = '# A\n\na\n\n# B\n\nb\n\n# C\n\nc\n';
|
|
78
|
-
expect(
|
|
79
|
-
moveHeadingSectionInSource(source, lineOf(source, '# C'), lineOf(source, '# A'), 'before'),
|
|
80
|
-
).toBe('# C\n\nc\n\n# A\n\na\n\n# B\n\nb\n');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('preserves CRLF and the absence of a final newline', () => {
|
|
84
|
-
const source = '# A\r\n\r\na\r\n\r\n# B\r\n\r\nb';
|
|
85
|
-
expect(
|
|
86
|
-
moveHeadingSectionInSource(source, lineOf(source, '# B'), lineOf(source, '# A'), 'before'),
|
|
87
|
-
).toBe('# B\r\n\r\nb\r\n\r\n# A\r\n\r\na');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('leaves frontmatter and preamble bytes in place', () => {
|
|
91
|
-
const source = [
|
|
92
|
-
'---',
|
|
93
|
-
'title: Example',
|
|
94
|
-
'---',
|
|
95
|
-
'',
|
|
96
|
-
'Preamble stays here.',
|
|
97
|
-
'',
|
|
98
|
-
'# A {[sectionHeader]}',
|
|
99
|
-
'',
|
|
100
|
-
'a',
|
|
101
|
-
'',
|
|
102
|
-
'# B {#custom .wide}',
|
|
103
|
-
'',
|
|
104
|
-
'b',
|
|
105
|
-
'',
|
|
106
|
-
].join('\n');
|
|
107
|
-
const prefix = source.slice(0, source.indexOf('# A'));
|
|
108
|
-
|
|
109
|
-
const next = moveHeadingSectionInSource(
|
|
110
|
-
source,
|
|
111
|
-
lineOf(source, '# B'),
|
|
112
|
-
lineOf(source, '# A'),
|
|
113
|
-
'before',
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
expect(next?.startsWith(prefix)).toBe(true);
|
|
117
|
-
expect(next).toContain('# B {#custom .wide}\n\nb\n\n# A {[sectionHeader]}\n\na\n');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('ignores heading lookalikes inside fenced code', () => {
|
|
121
|
-
const source = ['# A', '', '```md', '# not an outline heading', '```', '', '# B', '', 'b'].join(
|
|
122
|
-
'\n',
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const next = moveHeadingSectionInSource(
|
|
126
|
-
source,
|
|
127
|
-
lineOf(source, '# A'),
|
|
128
|
-
lineOf(source, '# B'),
|
|
129
|
-
'after',
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
expect(next).toBe(
|
|
133
|
-
['# B', '', 'b', '', '# A', '', '```md', '# not an outline heading', '```'].join('\n'),
|
|
134
|
-
);
|
|
135
|
-
expect(
|
|
136
|
-
moveHeadingSectionInSource(
|
|
137
|
-
source,
|
|
138
|
-
lineOf(source, '# not an outline heading'),
|
|
139
|
-
lineOf(source, '# B'),
|
|
140
|
-
'before',
|
|
141
|
-
),
|
|
142
|
-
).toBeNull();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('supports duplicate heading text and Setext headings by source line', () => {
|
|
146
|
-
const source = [
|
|
147
|
-
'Document',
|
|
148
|
-
'========',
|
|
149
|
-
'',
|
|
150
|
-
'First duplicate',
|
|
151
|
-
'---------------',
|
|
152
|
-
'',
|
|
153
|
-
'one',
|
|
154
|
-
'',
|
|
155
|
-
'First duplicate',
|
|
156
|
-
'---------------',
|
|
157
|
-
'',
|
|
158
|
-
'two',
|
|
159
|
-
].join('\n');
|
|
160
|
-
const firstLine = lineOf(source, 'First duplicate');
|
|
161
|
-
const secondLine = source
|
|
162
|
-
.slice(0, source.lastIndexOf('First duplicate'))
|
|
163
|
-
.split(/\r\n|\r|\n/).length;
|
|
164
|
-
|
|
165
|
-
const next = moveHeadingSectionInSource(source, secondLine, firstLine, 'before');
|
|
166
|
-
|
|
167
|
-
expect(next).toBe(
|
|
168
|
-
[
|
|
169
|
-
'Document',
|
|
170
|
-
'========',
|
|
171
|
-
'',
|
|
172
|
-
'First duplicate',
|
|
173
|
-
'---------------',
|
|
174
|
-
'',
|
|
175
|
-
'two',
|
|
176
|
-
'',
|
|
177
|
-
'First duplicate',
|
|
178
|
-
'---------------',
|
|
179
|
-
'',
|
|
180
|
-
'one',
|
|
181
|
-
].join('\n'),
|
|
182
|
-
);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('rejects hierarchy changes, stale lines, self-drops, and adjacent no-ops', () => {
|
|
186
|
-
const source = [
|
|
187
|
-
'# Parent A',
|
|
188
|
-
'',
|
|
189
|
-
'## Child A',
|
|
190
|
-
'',
|
|
191
|
-
'### Detail',
|
|
192
|
-
'',
|
|
193
|
-
'# Parent B',
|
|
194
|
-
'',
|
|
195
|
-
'## Child B',
|
|
196
|
-
'',
|
|
197
|
-
'### Skipped sibling',
|
|
198
|
-
'',
|
|
199
|
-
'## Last child',
|
|
200
|
-
].join('\n');
|
|
201
|
-
const parentA = lineOf(source, '# Parent A');
|
|
202
|
-
const parentB = lineOf(source, '# Parent B');
|
|
203
|
-
const childA = lineOf(source, '## Child A');
|
|
204
|
-
const childB = lineOf(source, '## Child B');
|
|
205
|
-
const detail = lineOf(source, '### Detail');
|
|
206
|
-
const skipped = lineOf(source, '### Skipped sibling');
|
|
207
|
-
const lastChild = lineOf(source, '## Last child');
|
|
208
|
-
|
|
209
|
-
expect(moveHeadingSectionInSource(source, parentA, parentA, 'before')).toBeNull();
|
|
210
|
-
expect(moveHeadingSectionInSource(source, childA, detail, 'before')).toBeNull();
|
|
211
|
-
expect(moveHeadingSectionInSource(source, childA, childB, 'before')).toBeNull();
|
|
212
|
-
expect(moveHeadingSectionInSource(source, skipped, lastChild, 'before')).toBeNull();
|
|
213
|
-
expect(moveHeadingSectionInSource(source, parentA, parentB, 'before')).toBeNull();
|
|
214
|
-
expect(moveHeadingSectionInSource(source, parentB, parentA, 'after')).toBeNull();
|
|
215
|
-
expect(moveHeadingSectionInSource(source, 999, parentA, 'before')).toBeNull();
|
|
216
|
-
});
|
|
217
|
-
});
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
-
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
4
|
-
import { resolveTheme } from '@bendyline/squisq/schemas';
|
|
5
|
-
import { PlainHtmlPreview } from '../PlainHtmlPreview';
|
|
6
|
-
|
|
7
|
-
function getIframeSrcDoc(): string {
|
|
8
|
-
const iframe = screen.getByTestId('plain-html-preview') as HTMLIFrameElement;
|
|
9
|
-
return iframe.srcdoc;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe('PlainHtmlPreview', () => {
|
|
13
|
-
it('renders the markdown into the iframe srcDoc', async () => {
|
|
14
|
-
render(<PlainHtmlPreview markdown={'# Hello\n\nWorld'} />);
|
|
15
|
-
await waitFor(() => expect(getIframeSrcDoc()).toContain('<!DOCTYPE html>'));
|
|
16
|
-
const srcdoc = getIframeSrcDoc();
|
|
17
|
-
expect(srcdoc).toContain('<h1>Hello</h1>');
|
|
18
|
-
expect(srcdoc).toContain('<p>World</p>');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('refuses scripts via sandbox attribute (allow-same-origin only)', () => {
|
|
22
|
-
render(<PlainHtmlPreview markdown={'hi'} />);
|
|
23
|
-
const iframe = screen.getByTestId('plain-html-preview') as HTMLIFrameElement;
|
|
24
|
-
const sandbox = iframe.getAttribute('sandbox') ?? '';
|
|
25
|
-
expect(sandbox).toContain('allow-same-origin');
|
|
26
|
-
expect(sandbox).not.toContain('allow-scripts');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('globally scrolls the iframe with up and down arrows when enabled', () => {
|
|
30
|
-
render(<PlainHtmlPreview markdown={'# Scroll me'} globalKeyboardShortcuts />);
|
|
31
|
-
const iframe = screen.getByTestId('plain-html-preview') as HTMLIFrameElement;
|
|
32
|
-
const scrollBy = vi.fn();
|
|
33
|
-
Object.defineProperty(iframe.contentWindow, 'scrollBy', {
|
|
34
|
-
configurable: true,
|
|
35
|
-
value: scrollBy,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
fireEvent.keyDown(document, { key: 'ArrowDown' });
|
|
39
|
-
fireEvent.keyDown(document, { key: 'ArrowUp' });
|
|
40
|
-
|
|
41
|
-
expect(scrollBy).toHaveBeenNthCalledWith(1, { top: 64, behavior: 'smooth' });
|
|
42
|
-
expect(scrollBy).toHaveBeenNthCalledWith(2, { top: -64, behavior: 'smooth' });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('applies pre-resolved images via the images prop', async () => {
|
|
46
|
-
render(
|
|
47
|
-
<PlainHtmlPreview
|
|
48
|
-
markdown={''}
|
|
49
|
-
images={new Map([['a.jpg', 'data:image/png;base64,AAA']])}
|
|
50
|
-
/>,
|
|
51
|
-
);
|
|
52
|
-
await waitFor(() => expect(getIframeSrcDoc()).toContain('src="data:image/png;base64,AAA"'));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('resolves image URLs through the supplied mediaProvider', async () => {
|
|
56
|
-
const provider: MediaProvider = {
|
|
57
|
-
resolveUrl: vi.fn(async (ref: string) => `blob:resolved/${ref}`),
|
|
58
|
-
listMedia: vi.fn(async () => []),
|
|
59
|
-
addMedia: vi.fn(async () => ''),
|
|
60
|
-
removeMedia: vi.fn(async () => {}),
|
|
61
|
-
dispose: vi.fn(() => {}),
|
|
62
|
-
};
|
|
63
|
-
render(<PlainHtmlPreview markdown={''} mediaProvider={provider} />);
|
|
64
|
-
await waitFor(() => {
|
|
65
|
-
expect(getIframeSrcDoc()).toContain('src="blob:resolved/cat.jpg"');
|
|
66
|
-
});
|
|
67
|
-
expect(provider.resolveUrl).toHaveBeenCalledWith('cat.jpg');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('does not resolve external URLs through the provider', async () => {
|
|
71
|
-
const provider: MediaProvider = {
|
|
72
|
-
resolveUrl: vi.fn(async (ref: string) => `blob:should-not-fire/${ref}`),
|
|
73
|
-
listMedia: vi.fn(async () => []),
|
|
74
|
-
addMedia: vi.fn(async () => ''),
|
|
75
|
-
removeMedia: vi.fn(async () => {}),
|
|
76
|
-
dispose: vi.fn(() => {}),
|
|
77
|
-
};
|
|
78
|
-
render(
|
|
79
|
-
<PlainHtmlPreview markdown={''} mediaProvider={provider} />,
|
|
80
|
-
);
|
|
81
|
-
await waitFor(() => {
|
|
82
|
-
expect(getIframeSrcDoc()).toContain('src="https://example.com/x.jpg"');
|
|
83
|
-
});
|
|
84
|
-
expect(provider.resolveUrl).not.toHaveBeenCalled();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('applies the supplied theme to the iframe document', async () => {
|
|
88
|
-
const theme = resolveTheme('warm-earth');
|
|
89
|
-
render(<PlainHtmlPreview markdown={'# Hi'} theme={theme} />);
|
|
90
|
-
await waitFor(() =>
|
|
91
|
-
expect(getIframeSrcDoc()).toContain(`--plain-bg: ${theme.colors.background};`),
|
|
92
|
-
);
|
|
93
|
-
const srcdoc = getIframeSrcDoc();
|
|
94
|
-
expect(srcdoc).toContain(`--plain-primary: ${theme.colors.primary};`);
|
|
95
|
-
expect(srcdoc).toContain('--plain-body-font:');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('loads Google Fonts for themes that reference google-hosted faces', async () => {
|
|
99
|
-
const theme = resolveTheme('documentary');
|
|
100
|
-
render(<PlainHtmlPreview markdown={'# Hi'} theme={theme} />);
|
|
101
|
-
await waitFor(() => expect(getIframeSrcDoc()).toContain('https://fonts.googleapis.com/css2?'));
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('resolves raw HTML <img> tags too (resized image case)', async () => {
|
|
105
|
-
// Resized images round-trip through the markdown source as raw
|
|
106
|
-
// HTML, which the preview must scan to keep the iframe's <img>
|
|
107
|
-
// pointing at a fetchable blob URL.
|
|
108
|
-
const provider: MediaProvider = {
|
|
109
|
-
resolveUrl: vi.fn(async (ref: string) => `blob:r/${ref}`),
|
|
110
|
-
listMedia: vi.fn(async () => []),
|
|
111
|
-
addMedia: vi.fn(async () => ''),
|
|
112
|
-
removeMedia: vi.fn(async () => {}),
|
|
113
|
-
dispose: vi.fn(() => {}),
|
|
114
|
-
};
|
|
115
|
-
render(
|
|
116
|
-
<PlainHtmlPreview
|
|
117
|
-
markdown={'<img alt="resized" src="resized.png" width="194">'}
|
|
118
|
-
mediaProvider={provider}
|
|
119
|
-
/>,
|
|
120
|
-
);
|
|
121
|
-
await waitFor(() => {
|
|
122
|
-
expect(getIframeSrcDoc()).toContain('src="blob:r/resized.png"');
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|