Skip to content

Commit 145693d

Browse files
committed
feat(document-api): add scope: block to format.apply for full-paragraph formatting
1 parent 9aa655b commit 145693d

5 files changed

Lines changed: 59 additions & 13 deletions

File tree

packages/document-api/src/contract/schemas.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4878,8 +4878,14 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
48784878
description:
48794879
'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.',
48804880
},
4881+
scope: {
4882+
type: 'string',
4883+
enum: ['match', 'block'],
4884+
description:
4885+
'When "block", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use "block" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: "match".',
4886+
},
48814887
},
4882-
[], // Neither field is individually required — at least one must be present
4888+
[], // No individual field is required — at least one must be present
48834889
),
48844890
},
48854891
['id', 'op', 'where', 'args'],

packages/document-api/src/types/mutation-plan.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ export type StyleApplyStep = {
119119
args: {
120120
inline?: InlineRunPatch;
121121
alignment?: 'left' | 'center' | 'right' | 'justify';
122+
/** When "block", inline formatting expands to cover the entire parent textblock(s), not just the matched range. Default: "match". */
123+
scope?: 'match' | 'block';
122124
};
123125
};
124126

packages/sdk/tools/prompt-templates/system-prompt-core.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,19 @@ superdoc_edit({action: "insert", type: "markdown",
146146
value: "# Executive Summary\n\nThis agreement sets forth the principal terms..."})
147147
```
148148

149-
**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts both `inline` (text styles) AND `alignment` (paragraph alignment) — one step per block.
149+
**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts `inline` (text styles), `alignment` (paragraph alignment), and `scope` — combine them all in one step per block.
150+
151+
ALWAYS use `scope: "block"` after markdown inserts. This makes the formatting cover the entire paragraph, not just the matched text pattern. The pattern only needs to uniquely identify which paragraph — a short prefix is enough.
150152

151153
Example: if the document has centered, underlined, 12pt headings and justified 12pt body text:
152154
```
153155
superdoc_mutations({action: "apply", atomic: true, steps: [
154-
{id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center"}},
155-
{id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify"}}
156+
{id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}},
157+
{id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify", scope: "block"}}
156158
]})
157159
```
158160

159-
CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use ONE format.apply step per block with both `inline` and `alignment` combined.
161+
CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use `scope: "block"` so the formatting covers the ENTIRE paragraph, not just the matched text.
160162

161163
Total: 3 calls (read + insert + format-all-in-one-batch). Never more.
162164

packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ superdoc_edit({action: "insert", type: "markdown",
3030
```
3131
Valid placements: "before", "after", "insideStart", "insideEnd". Without target, content appends at document end.
3232

33-
**Formatting — each format.apply step accepts both `inline` AND `alignment`:**
33+
**Formatting — use `scope: "block"` to format entire paragraphs after markdown insert:**
3434
```
3535
superdoc_mutations({action: "apply", atomic: true, steps: [
36-
{id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center"}},
37-
{id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "body paragraph text"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "justify"}}
36+
{id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}},
37+
{id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "justify", scope: "block"}}
3838
]})
3939
```
40-
One format.apply step per block. Combine `inline` (text styles) and `alignment` (paragraph alignment) in the same step. Do NOT use separate superdoc_format calls.
40+
One format.apply step per block. Combine `inline`, `alignment`, and `scope: "block"` in each step. The pattern only needs to identify which paragraph `scope: "block"` formats the entire paragraph, not just the matched text.
4141

4242
**When to use which tool:**
4343
- Creating headings, paragraphs, or any block content → `superdoc_edit` with type "markdown" (preferred, even for a single heading + paragraph)

packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -841,15 +841,45 @@ function applyAlignmentToRange(tr: Transaction, absFrom: number, absTo: number,
841841
return changed;
842842
}
843843

844+
/**
845+
* Expands a position range to cover the full content of all textblock nodes
846+
* that overlap with it. Used when scope: "block" is set on a format.apply step.
847+
*/
848+
function expandToBlockBoundaries(
849+
doc: import('prosemirror-model').Node,
850+
from: number,
851+
to: number,
852+
): { from: number; to: number } {
853+
let expandedFrom = from;
854+
let expandedTo = to;
855+
856+
doc.nodesBetween(from, to, (node, pos) => {
857+
if (!node.isTextblock) return;
858+
const blockContentStart = pos + 1;
859+
const blockContentEnd = pos + node.nodeSize - 1;
860+
expandedFrom = Math.min(expandedFrom, blockContentStart);
861+
expandedTo = Math.max(expandedTo, blockContentEnd);
862+
});
863+
864+
return { from: expandedFrom, to: expandedTo };
865+
}
866+
844867
export function executeStyleApply(
845868
editor: Editor,
846869
tr: Transaction,
847870
target: CompiledRangeTarget,
848871
step: StyleApplyStep,
849872
mapping: Mapping,
850873
): { changed: boolean } {
851-
const absFrom = mapping.map(target.absFrom);
852-
const absTo = mapping.map(target.absTo);
874+
let absFrom = mapping.map(target.absFrom);
875+
let absTo = mapping.map(target.absTo);
876+
877+
// Expand to full block boundaries when scope is "block"
878+
if (step.args.scope === 'block') {
879+
const expanded = expandToBlockBoundaries(tr.doc, absFrom, absTo);
880+
absFrom = expanded.from;
881+
absTo = expanded.to;
882+
}
853883

854884
let changed = false;
855885

@@ -1002,8 +1032,14 @@ export function executeSpanStyleApply(
10021032
// Apply marks uniformly across the full span
10031033
const firstSeg = target.segments[0];
10041034
const lastSeg = target.segments[target.segments.length - 1];
1005-
const absFrom = mapping.map(firstSeg.absFrom, 1);
1006-
const absTo = mapping.map(lastSeg.absTo, -1);
1035+
let absFrom = mapping.map(firstSeg.absFrom, 1);
1036+
let absTo = mapping.map(lastSeg.absTo, -1);
1037+
1038+
if (step.args.scope === 'block') {
1039+
const expanded = expandToBlockBoundaries(tr.doc, absFrom, absTo);
1040+
absFrom = expanded.from;
1041+
absTo = expanded.to;
1042+
}
10071043

10081044
let changed = false;
10091045

0 commit comments

Comments
 (0)