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
4 changes: 4 additions & 0 deletions apps/cli/scripts/export-sdk-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const INTENT_NAMES = {
'doc.replace': 'replace_content',
'doc.delete': 'delete_content',
'doc.format.apply': 'format_apply',
'doc.format.fontSize': 'format_font_size',
'doc.format.fontFamily': 'format_font_family',
'doc.format.color': 'format_color',
'doc.format.align': 'format_align',
'doc.create.paragraph': 'create_paragraph',
'doc.create.heading': 'create_heading',
'doc.lists.list': 'list_lists',
Expand Down
76 changes: 76 additions & 0 deletions apps/cli/src/__tests__/conformance/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,82 @@ export const SUCCESS_SCENARIOS = {
],
};
},
'doc.format.fontSize': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-format-font-size-success');
const docPath = await harness.copyFixtureDoc('doc-format-font-size');
const target = await harness.firstTextRange(docPath, stateDir);
return {
stateDir,
args: [
'format',
'font-size',
docPath,
'--target-json',
JSON.stringify(target),
'--value-json',
JSON.stringify('14pt'),
'--out',
harness.createOutputPath('doc-format-font-size-output'),
],
};
},
'doc.format.fontFamily': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-format-font-family-success');
const docPath = await harness.copyFixtureDoc('doc-format-font-family');
const target = await harness.firstTextRange(docPath, stateDir);
return {
stateDir,
args: [
'format',
'font-family',
docPath,
'--target-json',
JSON.stringify(target),
'--value-json',
JSON.stringify('Arial'),
'--out',
harness.createOutputPath('doc-format-font-family-output'),
],
};
},
'doc.format.color': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-format-color-success');
const docPath = await harness.copyFixtureDoc('doc-format-color');
const target = await harness.firstTextRange(docPath, stateDir);
return {
stateDir,
args: [
'format',
'color',
docPath,
'--target-json',
JSON.stringify(target),
'--value-json',
JSON.stringify('#ff0000'),
'--out',
harness.createOutputPath('doc-format-color-output'),
],
};
},
'doc.format.align': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-format-align-success');
const docPath = await harness.copyFixtureDoc('doc-format-align');
const target = await harness.firstTextRange(docPath, stateDir);
return {
stateDir,
args: [
'format',
'align',
docPath,
'--target-json',
JSON.stringify(target),
'--alignment-json',
JSON.stringify('center'),
'--out',
harness.createOutputPath('doc-format-align-output'),
],
};
},
'doc.trackChanges.list': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
const stateDir = await harness.createStateDir('doc-track-changes-list-success');
const fixture = await harness.addTrackedChangeFixture(stateDir, 'doc-track-changes-list');
Expand Down
20 changes: 20 additions & 0 deletions apps/cli/src/cli/operation-hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const SUCCESS_VERB: Record<CliExposedOperationId, string> = {
replace: 'replaced text',
delete: 'deleted text',
'format.apply': 'applied style',
'format.fontSize': 'set font size',
'format.fontFamily': 'set font family',
'format.color': 'set text color',
'format.align': 'set alignment',
'create.paragraph': 'created paragraph',
'create.heading': 'created heading',
'lists.list': 'listed items',
Expand Down Expand Up @@ -93,6 +97,10 @@ export const OUTPUT_FORMAT: Record<CliExposedOperationId, OutputFormat> = {
replace: 'mutationReceipt',
delete: 'mutationReceipt',
'format.apply': 'mutationReceipt',
'format.fontSize': 'mutationReceipt',
'format.fontFamily': 'mutationReceipt',
'format.color': 'mutationReceipt',
'format.align': 'mutationReceipt',
'create.paragraph': 'createResult',
'create.heading': 'createResult',
'lists.list': 'listResult',
Expand Down Expand Up @@ -138,6 +146,10 @@ export const RESPONSE_ENVELOPE_KEY: Record<CliExposedOperationId, string | null>
replace: null,
delete: null,
'format.apply': null,
'format.fontSize': null,
'format.fontFamily': null,
'format.color': null,
'format.align': null,
'create.paragraph': 'result',
'create.heading': 'result',
'lists.list': 'result',
Expand Down Expand Up @@ -178,6 +190,10 @@ export const RESPONSE_VALIDATION_KEY: Partial<Record<CliExposedOperationId, stri
replace: 'receipt',
delete: 'receipt',
'format.apply': 'receipt',
'format.fontSize': 'receipt',
'format.fontFamily': 'receipt',
'format.color': 'receipt',
'format.align': 'receipt',
};

// ---------------------------------------------------------------------------
Expand All @@ -200,6 +216,10 @@ export const OPERATION_FAMILY: Record<CliExposedOperationId, OperationFamily> =
replace: 'textMutation',
delete: 'textMutation',
'format.apply': 'textMutation',
'format.fontSize': 'textMutation',
'format.fontFamily': 'textMutation',
'format.color': 'textMutation',
'format.align': 'textMutation',
'create.paragraph': 'create',
'create.heading': 'create',
'lists.list': 'lists',
Expand Down
8 changes: 6 additions & 2 deletions apps/cli/src/cli/operation-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ const EXTRA_CLI_PARAMS: Partial<Record<string, CliOperationParamSpec[]>> = {
'doc.replace': [...TEXT_TARGET_FLAT_PARAMS],
'doc.delete': [...TEXT_TARGET_FLAT_PARAMS],
'doc.format.apply': [...TEXT_TARGET_FLAT_PARAMS],
'doc.format.fontSize': [...TEXT_TARGET_FLAT_PARAMS],
'doc.format.fontFamily': [...TEXT_TARGET_FLAT_PARAMS],
'doc.format.color': [...TEXT_TARGET_FLAT_PARAMS],
'doc.format.align': [...TEXT_TARGET_FLAT_PARAMS],
'doc.comments.create': [...TEXT_TARGET_FLAT_PARAMS],
'doc.comments.patch': [...TEXT_TARGET_FLAT_PARAMS],
// List operations: flat flag (--node-id) as shortcut for --target-json, plus --input-json
Expand Down Expand Up @@ -398,9 +402,9 @@ const CLI_ONLY_METADATA: Record<CliOnlyOperationId, CliOperationMetadata> = {
'doc.open': {
command: 'open',
positionalParams: ['doc'],
docRequirement: 'required',
docRequirement: 'none',
params: [
{ name: 'doc', kind: 'doc', type: 'string', required: true },
{ name: 'doc', kind: 'doc', type: 'string' },
SESSION_PARAM,
{ name: 'collaboration', kind: 'jsonFlag', flag: 'collaboration-json', type: 'json' },
{ name: 'collabDocumentId', kind: 'flag', flag: 'collab-document-id', type: 'string' },
Expand Down
22 changes: 13 additions & 9 deletions apps/cli/src/commands/open.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getBooleanOption, getStringOption, requireDocArg, resolveJsonInput } from '../lib/args';
import { getBooleanOption, getStringOption, resolveDocArg, resolveJsonInput } from '../lib/args';
import { parseCollaborationInput, resolveCollaborationProfile } from '../lib/collaboration';
import {
getProjectRoot,
Expand Down Expand Up @@ -27,21 +27,21 @@ export async function runOpen(tokens: string[], context: CommandContext): Promis
command: 'open',
data: {
usage: [
'superdoc open <doc> [--session <id>]',
'superdoc open <doc> --collaboration-json "{...}" [--session <id>]',
'superdoc open [doc] [--session <id>]',
'superdoc open [doc] --collaboration-json "{...}" [--session <id>]',
],
},
pretty: [
'Usage:',
' superdoc open <doc> [--session <id>]',
' superdoc open <doc> --collaboration-json "{...}" [--session <id>]',
' superdoc open [doc] [--session <id>]',
' superdoc open [doc] --collaboration-json "{...}" [--session <id>]',
].join('\n'),
};
}

const { doc } = requireDocArg(parsed, 'open');
const { doc } = resolveDocArg(parsed, 'open');

const sessionId = context.sessionId ?? generateSessionId(doc);
const sessionId = context.sessionId ?? generateSessionId(doc ?? 'blank');
const collaborationPayload = await resolveJsonInput(parsed, 'collaboration');
const collabUrl = getStringOption(parsed, 'collab-url');
const collabDocumentId = getStringOption(parsed, 'collab-document-id');
Expand Down Expand Up @@ -98,8 +98,12 @@ export async function runOpen(tokens: string[], context: CommandContext): Promis
);
}

if (collaboration && doc == null) {
throw new CliError('MISSING_REQUIRED', 'open: a document path is required when using collaboration.');
}

const opened = collaboration
? await openCollaborativeDocument(doc, context.io, collaboration)
? await openCollaborativeDocument(doc!, context.io, collaboration)
: await openDocument(doc, context.io);
let adoptedToHostPool = false;
try {
Expand Down Expand Up @@ -143,7 +147,7 @@ export async function runOpen(tokens: string[], context: CommandContext): Promis
openedAt: metadata.openedAt,
updatedAt: metadata.updatedAt,
},
pretty: `Opened ${metadata.sourcePath ?? '<stdin>'} in context ${metadata.contextId} (${metadata.sessionType})`,
pretty: `Opened ${metadata.sourcePath ?? (metadata.source === 'blank' ? '<blank>' : '<stdin>')} in context ${metadata.contextId} (${metadata.sessionType})`,
};
} finally {
if (!adoptedToHostPool) {
Expand Down
6 changes: 3 additions & 3 deletions apps/cli/src/lib/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type SessionType = 'local' | 'collab';
export type ContextMetadata = {
contextId: string;
projectRoot: string;
source: 'path' | 'stdin';
source: 'path' | 'stdin' | 'blank';
sourcePath?: string;
workingDocPath: string;
dirty: boolean;
Expand All @@ -48,7 +48,7 @@ export type ContextPaths = {

export type ProjectSessionSummary = {
sessionId: string;
source: 'path' | 'stdin';
source: 'path' | 'stdin' | 'blank';
sourcePath?: string;
dirty: boolean;
revision: number;
Expand Down Expand Up @@ -653,7 +653,7 @@ export function createInitialContextMetadata(
paths: ContextPaths,
contextId: string,
input: {
source: 'path' | 'stdin';
source: 'path' | 'stdin' | 'blank';
sourcePath?: string;
sourceSnapshot?: SourceSnapshot;
sessionType?: SessionType;
Expand Down
23 changes: 19 additions & 4 deletions apps/cli/src/lib/document.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFile, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { Editor } from 'superdoc/super-editor';
import { BLANK_DOCX_BASE64 } from '@superdoc/super-editor/blank-docx';
import { getDocumentApiAdapters } from '@superdoc/super-editor/document-api-adapters';

import { createDocumentApi, type DocumentApi } from '@superdoc/document-api';
Expand Down Expand Up @@ -80,14 +81,28 @@ async function readDocumentSource(doc: string, io: CliIO): Promise<{ bytes: Uint
};
}

export async function openDocument(doc: string, io: CliIO, options: OpenDocumentOptions = {}): Promise<OpenedDocument> {
const { bytes, meta } = await readDocumentSource(doc, io);
export async function openDocument(
doc: string | undefined,
io: CliIO,
options: OpenDocumentOptions = {},
): Promise<OpenedDocument> {
let source: Uint8Array;
let meta: DocumentSourceMeta;

if (doc != null) {
const result = await readDocumentSource(doc, io);
source = result.bytes;
meta = result.meta;
} else {
source = Buffer.from(BLANK_DOCX_BASE64, 'base64');
meta = { source: 'blank', byteLength: source.byteLength };
}

let editor: Editor;
try {
const isTest = process.env.NODE_ENV === 'test';
editor = await Editor.open(Buffer.from(bytes), {
documentId: options.documentId ?? meta.path ?? 'stdin.docx',
editor = await Editor.open(Buffer.from(source), {
documentId: options.documentId ?? meta.path ?? 'blank.docx',
user: { id: 'cli', name: 'CLI' },
...(isTest ? { telemetry: { enabled: false } } : {}),
ydoc: options.ydoc,
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/src/lib/invoke-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ const TEXT_TARGET_OPERATIONS = new Set<CliExposedOperationId>([
'replace',
'delete',
'format.apply',
'format.fontSize',
'format.fontFamily',
'format.color',
'format.align',
'comments.create',
'comments.patch',
]);
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/lib/mutation-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const STATELESS_OUT_EXEMPT = new Set<CliExposedOperationId>([]);

type DocumentPayload = {
path?: string;
source: 'path' | 'stdin';
source: 'path' | 'stdin' | 'blank';
byteLength: number;
revision: number;
};
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/lib/read-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { extractInvokeInput } from './invoke-input.js';

type DocumentPayload = {
path?: string;
source: 'path' | 'stdin';
source: 'path' | 'stdin' | 'blank';
byteLength: number;
revision: number;
};
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/src/lib/special-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ export const POST_INVOKE_HOOKS: Partial<Record<CliExposedOperationId, PostInvoke
replace: flattenTextMutationReceipt,
delete: flattenTextMutationReceipt,
'format.apply': flattenTextMutationReceipt,
'format.fontSize': flattenTextMutationReceipt,
'format.fontFamily': flattenTextMutationReceipt,
'format.color': flattenTextMutationReceipt,
'format.align': flattenTextMutationReceipt,
// getNodeById: merge nodeId from input into result for pretty output
getNodeById: (result, context) => {
const record = asRecord(result);
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export interface CommandContext {
}

export interface DocumentSourceMeta {
source: 'path' | 'stdin';
source: 'path' | 'stdin' | 'blank';
path?: string;
byteLength: number;
}
5 changes: 4 additions & 1 deletion apps/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"skipLibCheck": true,
"types": ["bun"],
"paths": {
"@superdoc/super-editor/document-api-adapters": ["../../packages/super-editor/src/document-api-adapters/index.ts"]
"@superdoc/super-editor/document-api-adapters": [
"../../packages/super-editor/src/document-api-adapters/index.ts"
],
"@superdoc/super-editor/blank-docx": ["../../packages/super-editor/src/core/blank-docx.ts"]
}
},
"include": ["src"]
Expand Down
6 changes: 5 additions & 1 deletion apps/docs/document-api/available-operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Use the tables below to see what operations are available and where each one is
| Comments | 5 | 0 | 5 | [Reference](/document-api/reference/comments/index) |
| Core | 8 | 0 | 8 | [Reference](/document-api/reference/core/index) |
| Create | 2 | 0 | 2 | [Reference](/document-api/reference/create/index) |
| Format | 1 | 4 | 5 | [Reference](/document-api/reference/format/index) |
| Format | 5 | 4 | 9 | [Reference](/document-api/reference/format/index) |
| Lists | 8 | 0 | 8 | [Reference](/document-api/reference/lists/index) |
| Mutations | 2 | 0 | 2 | [Reference](/document-api/reference/mutations/index) |
| Query | 1 | 0 | 1 | [Reference](/document-api/reference/query/index) |
Expand All @@ -43,6 +43,10 @@ Use the tables below to see what operations are available and where each one is
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.create.paragraph(...)</code></span> | [`create.paragraph`](/document-api/reference/create/paragraph) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.create.heading(...)</code></span> | [`create.heading`](/document-api/reference/create/heading) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.apply(...)</code></span> | [`format.apply`](/document-api/reference/format/apply) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.fontSize(...)</code></span> | [`format.fontSize`](/document-api/reference/format/font-size) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.fontFamily(...)</code></span> | [`format.fontFamily`](/document-api/reference/format/font-family) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.color(...)</code></span> | [`format.color`](/document-api/reference/format/color) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.align(...)</code></span> | [`format.align`](/document-api/reference/format/align) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.bold(...)</code></span> | [`format.apply`](/document-api/reference/format/apply) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.italic(...)</code></span> | [`format.apply`](/document-api/reference/format/apply) |
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.underline(...)</code></span> | [`format.apply`](/document-api/reference/format/apply) |
Expand Down
Loading
Loading