Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions .github/scripts/risk-assess.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@
*/

import { execSync } from 'node:child_process';
import { writeFileSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { writeFileSync } from 'node:fs';

// Allow running inside a Claude Code session
delete process.env.CLAUDECODE;

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO = process.env.REPO || 'superdoc-dev/superdoc';

/** Extract the first valid JSON object containing "level" from text. */
Expand Down Expand Up @@ -62,11 +59,6 @@ function run(cmd) {
return execSync(cmd, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }).trim();
}

function getPRInfo(pr) {
const json = run(`gh pr view ${pr} --repo ${REPO} --json title,files,changedFiles`);
return JSON.parse(json);
}

function getPRDiff(pr) {
return run(`gh pr diff ${pr} --repo ${REPO}`);
}
Expand Down Expand Up @@ -353,7 +345,6 @@ async function main() {
}

const forceDeep = flags.has('--deep');
const dryRun = flags.has('--dry-run');
const repoRoot = process.env.REPO_ROOT || run('git rev-parse --show-toplevel');

const results = [];
Expand Down
7 changes: 5 additions & 2 deletions demos/__tests__/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ test('demo loads without errors', async ({ page }) => {
if (msg.type() === 'error') errors.push(msg.text());
});

// Block telemetry requests during tests
await page.route('**/ingest.superdoc.dev/**', (route) => route.abort());
// Disable telemetry during tests by stubbing the ingest endpoint.
// Using fulfill (instead of abort) avoids browser console errors.
await page.route('**/ingest.superdoc.dev/**', (route) =>
route.fulfill({ status: 204, contentType: 'application/json', body: '{}' }),
);

await page.goto('/');
await expect(page.locator('body')).toBeVisible();
Expand Down
3 changes: 1 addition & 2 deletions demos/grading-papers/app/grading/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client"

import 'superdoc/style.css'
import { useState, useEffect, useRef } from "react"
import { useState, useEffect, useRef, use } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, Save, Send, Download, Printer, Menu, Bell, Search } from "lucide-react"
import { Button } from "@/components/ui/button"
Expand All @@ -13,7 +13,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Slider } from "@/components/ui/slider"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { SuperDoc } from "superdoc"
import { use } from 'react';
import { docMap } from './_doc-links';

export default function GradingPage({ params }: { params: Promise<{ id: string }> }) {
Expand Down
3 changes: 1 addition & 2 deletions demos/slack-redlining/cloud-function/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import fs from "fs";
import { readFile, unlink } from "fs/promises";
import express from "express";
import https from "https";
import path from "path";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import {
getAIResponse,
generateUploadDownloadUrls,
Expand Down
22 changes: 11 additions & 11 deletions demos/vue/src/components/DocumentEditor.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<div class="document-editor">
<div :key="documentKey" class="editor-container">
<div class="editor-container">
<div id="superdoc-toolbar" class="toolbar"></div>
<div id="superdoc" class="editor"></div>
</div>
</div>
</template>

<script setup>
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { SuperDoc } from 'superdoc';
import 'superdoc/style.css';

Expand All @@ -27,23 +27,23 @@ const emit = defineEmits(['editor-ready', 'editor-error']);

// Use ref to track the editor instance
const editor = ref(null);
const documentKey = ref(0);

// Function to safely destroy editor
const destroyEditor = () => {
if (editor.value) {
editor.value.destroy();
editor.value = null;
}
};

// Function to initialize editor
const initializeEditor = async () => {
try {
// Ensure cleanup of previous instance
// Ensure cleanup of previous instance before re-initializing.
destroyEditor();
// Increment key to force re-render
documentKey.value++;

// Wait one tick so the container refs stay stable during remount scenarios.
await nextTick();

// Create new editor instance
editor.value = new SuperDoc({
Expand All @@ -68,14 +68,14 @@ const initializeEditor = async () => {

// Watch for changes in props that should trigger re-initialization
watch(
() => [props.documentId, props.initialData, props.readOnly],
() => [props.initialData, props.readOnly],
() => {
initializeEditor();
void initializeEditor();
}
);

onMounted(() => {
initializeEditor();
void initializeEditor();
});

onUnmounted(() => {
Expand Down Expand Up @@ -110,4 +110,4 @@ onUnmounted(() => {
margin-top: 10px;
min-height: 400px; /* Ensure editor has minimum height */
}
</style>
</style>
2 changes: 1 addition & 1 deletion demos/word-addin/src/taskpane/taskpane.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

/* eslint-disable prettier/prettier */
/* global document, Office, Word */
/* eslint-disable no-use-before-define */

import SERVER_DOMAIN from '../server-domain.js';

Expand Down
Binary file not shown.
65 changes: 65 additions & 0 deletions e2e-tests/tests/visuals/layout-engine.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,70 @@ if (!shouldRun) {
});
});
});

const loadStructuredContentDocument = async (page) => {
const superEditor = await goToPageAndWaitForEditor(page, { layout: 1 });
const fileInput = page.locator('input[type="file"]');

await fileInput.setInputFiles('./test-data/structured-content/sdt-basic.docx');

await page.waitForFunction(() => window.superdoc !== undefined && window.editor !== undefined, null, {
polling: 100,
timeout: 10_000,
});

await page.waitForFunction(() => {
const toolbar = document.querySelector('#toolbar');
return toolbar && toolbar.children.length > 0;
});

return superEditor;
};

test('structured content: inline selection (sdt-basic.docx)', async ({ page }) => {
const superEditor = await loadStructuredContentDocument(page);
const inlineStructuredContent = page.locator('.superdoc-structured-content-inline').first();

await expect(inlineStructuredContent).toBeVisible();
await inlineStructuredContent.scrollIntoViewIfNeeded();
await inlineStructuredContent.hover();
await inlineStructuredContent.click({ force: true });
await expect(inlineStructuredContent).toHaveClass(/ProseMirror-selectednode/);
await page.waitForFunction(() => {
return document.querySelectorAll('.superdoc-structured-content-block.sdt-group-hover').length === 0;
});
const inlineEditorBox = await superEditor.boundingBox();
if (inlineEditorBox) {
await page.mouse.move(inlineEditorBox.x - 10, inlineEditorBox.y - 10);
}

await expect(superEditor).toHaveScreenshot();
});

test('structured content: block selection (sdt-basic.docx)', async ({ page }) => {
const superEditor = await loadStructuredContentDocument(page);
const blockStructuredContent = page.locator('.superdoc-structured-content-block').first();

await expect(blockStructuredContent).toBeVisible();
await blockStructuredContent.scrollIntoViewIfNeeded();
await blockStructuredContent.hover();
await blockStructuredContent.click({ force: true });
await expect(blockStructuredContent).toHaveClass(/ProseMirror-selectednode/);
const blockEditorBox = await superEditor.boundingBox();
if (blockEditorBox) {
await page.mouse.move(blockEditorBox.x - 10, blockEditorBox.y - 10);
}
await page.waitForFunction(
() => {
const block = document.querySelector('.superdoc-structured-content-block');
if (block?.matches(':hover')) return false;
return document.querySelectorAll('.superdoc-structured-content-block.sdt-group-hover').length === 0;
},
null,
{ timeout: 2_000 },
);

await expect(superEditor).toHaveScreenshot();
});
});
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export default [
// Examples (different environments and coding styles)
'examples/**',
'**/examples/**',
// Demos (different environments and dependency sets)
'demos/**',
'**/demos/**',
// Config files (CommonJS/different environments)
'**/*.config.js',
'**/*.cjs',
Expand Down
83 changes: 83 additions & 0 deletions packages/layout-engine/painters/dom/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2414,6 +2414,89 @@ describe('DomPainter', () => {
expect(fragmentAfter?.textContent).toContain('world!!!');
});

it('updates structured-content lock metadata when lockMode changes via setData', () => {
const lockedBlock: FlowBlock = {
kind: 'paragraph',
id: 'lock-mode-block',
runs: [{ text: 'Protected text', fontFamily: 'Arial', fontSize: 16, pmStart: 0, pmEnd: 14 }],
attrs: {
sdt: {
type: 'structuredContent',
scope: 'block',
id: 'sc-lock-mode-1',
alias: 'Protected Control',
lockMode: 'unlocked',
},
},
};

const lockedMeasure: Measure = {
kind: 'paragraph',
lines: [
{
fromRun: 0,
fromChar: 0,
toRun: 0,
toChar: 14,
width: 140,
ascent: 12,
descent: 4,
lineHeight: 20,
},
],
totalHeight: 20,
};

const lockedLayout: Layout = {
pageSize: { w: 400, h: 500 },
pages: [
{
number: 1,
fragments: [
{
kind: 'para',
blockId: 'lock-mode-block',
fromLine: 0,
toLine: 1,
x: 20,
y: 30,
width: 300,
pmStart: 0,
pmEnd: 14,
},
],
},
],
};

const painter = createDomPainter({ blocks: [lockedBlock], measures: [lockedMeasure] });
painter.paint(lockedLayout, mount);

const fragmentBefore = mount.querySelector('.superdoc-fragment') as HTMLElement;
expect(fragmentBefore.dataset.lockMode).toBe('unlocked');

const updatedLockedBlock: FlowBlock = {
kind: 'paragraph',
id: 'lock-mode-block',
runs: [{ text: 'Protected text', fontFamily: 'Arial', fontSize: 16, pmStart: 0, pmEnd: 14 }],
attrs: {
sdt: {
type: 'structuredContent',
scope: 'block',
id: 'sc-lock-mode-1',
alias: 'Protected Control',
lockMode: 'contentLocked',
},
},
};

painter.setData?.([updatedLockedBlock], [lockedMeasure]);
painter.paint(lockedLayout, mount);

const fragmentAfter = mount.querySelector('.superdoc-fragment') as HTMLElement;
expect(fragmentAfter.dataset.lockMode).toBe('contentLocked');
});

it('updates fragment positions in virtualized mode when layout changes without block diffs', () => {
const painter = createDomPainter({
blocks: [block],
Expand Down
Loading
Loading