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
12 changes: 11 additions & 1 deletion packages/super-editor/src/editors/v1/core/CommandService.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,17 @@ export class CommandService {
get commands() {
return Object.fromEntries(
Object.entries(rawCommands).map(([name, command]) => {
return [name, (...args) => command(...args)(props)];
// SD-3240: SurfaceCommandCallable types `props` as the public
// `CommandProps` shape, while the locally-built props here is
// a structurally-compatible JS object (the JS file's `state`
// helper returns `any` until typed). Cast at the boundary.
return [
name,
(...args) =>
command(...args)(
/** @type {import('./types/ChainedCommands.js').CommandProps} */ (/** @type {unknown} */ (props)),
),
];
}),
);
},
Expand Down
52 changes: 40 additions & 12 deletions packages/super-editor/src/editors/v1/core/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ExtensionService } from './ExtensionService.js';
import { CommandService } from './CommandService.js';
import { Attribute } from './Attribute.js';
import { SuperConverter } from '@core/super-converter/SuperConverter.js';
import type { EditorConverterSurface, EditorExtensionServiceSurface } from './types/EditorPublicSurfaces.js';
import {
Commands,
Editable,
Expand Down Expand Up @@ -253,10 +254,8 @@ export class Editor extends EventEmitter<EditorEventMap> {
*/
#commandService!: CommandService;

/**
* Service for managing extensions
*/
extensionService!: ExtensionService;
/** Extension service. See `EditorExtensionServiceSurface`. SD-3240. */
extensionService!: EditorExtensionServiceSurface;

/**
* Storage for extension data
Expand Down Expand Up @@ -348,7 +347,8 @@ export class Editor extends EventEmitter<EditorEventMap> {
/**
* The document converter instance
*/
converter!: SuperConverter;
/** Document converter handle. See `EditorConverterSurface`. SD-3240. */
converter!: EditorConverterSurface;

/**
* Toolbar instance (if attached)
Expand Down Expand Up @@ -648,7 +648,7 @@ export class Editor extends EventEmitter<EditorEventMap> {
if (!this.#telemetry || this.#documentOpenTracked) return;

try {
const documentCreatedAt = this.converter?.getDocumentCreatedTimestamp?.() || null;
const documentCreatedAt = this.converter?.getDocumentCreatedTimestamp?.() ?? null;
this.#telemetry.trackDocumentOpen(documentId, documentCreatedAt);
this.#documentOpenTracked = true;
} catch {
Expand Down Expand Up @@ -1108,7 +1108,14 @@ export class Editor extends EventEmitter<EditorEventMap> {
#initProtectionState(): void {
const protStorage = getProtectionStorage(this);
if (!protStorage) return;
const settingsRoot = this.converter ? readSettingsRoot(this.converter) : null;
// SD-3240: readSettingsRoot accepts a narrow `ConverterWithDocumentSettings`
// shape that overlaps with EditorConverterSurface but uses a
// different `pageStyles` typing (alternateHeaders flag). Cast to
// the local narrow interface. Both shapes are honest no-`any`
// contracts on the same runtime instance.
const settingsRoot = this.converter
? readSettingsRoot(this.converter as unknown as Parameters<typeof readSettingsRoot>[0])
: null;
protStorage.state = parseProtectionState(settingsRoot);
protStorage.initialized = true;
}
Expand Down Expand Up @@ -2115,7 +2122,15 @@ export class Editor extends EventEmitter<EditorEventMap> {

const isolatedExternalExtensions = externalExtensions.map((extension) => cloneExtensionInstance(extension));

this.extensionService = ExtensionService.create(allExtensions, isolatedExternalExtensions, this);
// SD-3240: ExtensionService.d.ts uses a `[key: string]: any` catchall
// for internal-implementation members. The runtime instance has the
// surface members; the cast bridges the structural gap without
// reintroducing `any` on the public field type.
this.extensionService = ExtensionService.create(
allExtensions,
isolatedExternalExtensions,
this,
) as unknown as EditorExtensionServiceSurface;
}

/**
Expand All @@ -2131,8 +2146,12 @@ export class Editor extends EventEmitter<EditorEventMap> {
* Create the document converter as this.converter.
*/
#createConverter(): void {
// SD-3240: SuperConverter.d.ts uses a `[key: string]: any` catchall
// for internal-implementation members. The runtime instance has the
// surface members; the cast bridges the structural gap without
// reintroducing `any` on the public field type.
if (this.options.converter) {
this.converter = this.options.converter as SuperConverter;
this.converter = this.options.converter as unknown as EditorConverterSurface;
} else {
this.converter = new SuperConverter({
docx: this.options.content,
Expand All @@ -2145,7 +2164,7 @@ export class Editor extends EventEmitter<EditorEventMap> {
mockDocument: this.options.mockDocument ?? null,
isNewFile: this.options.isNewFile ?? false,
trackedChangesOptions: this.options.trackedChanges ?? null,
});
}) as unknown as EditorConverterSurface;
}
}

Expand Down Expand Up @@ -2367,7 +2386,10 @@ export class Editor extends EventEmitter<EditorEventMap> {

const suppressedNames = new Set(
(this.extensionService?.extensions || [])
.filter((ext: { config?: { excludeFromSummaryJSON?: boolean } }) => {
.filter((ext) => {
// SD-3240: extension entries are typed but `excludeFromSummaryJSON`
// is a runtime opt-in on the config record (Options/Storage generics
// hide it). Cast at the read site to access the optional flag.
const config = (ext as { config?: { excludeFromSummaryJSON?: boolean } })?.config;
const suppressFlag = config?.excludeFromSummaryJSON;
return Boolean(suppressFlag);
Expand Down Expand Up @@ -3233,7 +3255,13 @@ export class Editor extends EventEmitter<EditorEventMap> {

this.#validateDocumentExport();

if (exportXmlOnly || exportJsonOnly) return documentXml;
// SD-3240: converter surface returns `string | Record<string, unknown>`
// (the JSON-only branch returns the intermediate xml-js tree).
// The Editor.exportDocx implementation signature here declares the
// outer union as `Record<string, string | null>` which is a pre-
// existing narrower shape than the runtime JSON tree. Cast at the
// bridge so the public surface stays honest.
if (exportXmlOnly || exportJsonOnly) return documentXml as string | Record<string, string | null>;

const customXml = this.converter.schemaToXml(this.converter.convertedXml['docProps/custom.xml'].elements[0]);
const styles = this.converter.schemaToXml(this.converter.convertedXml['word/styles.xml'].elements[0]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ type MinimalConverterContext = {
};

/**
* Extended Editor interface that includes the converter property.
* Used for type-safe access to header/footer data stored in the converter.
* SD-3240: `Editor.converter` is now typed as `EditorConverterSurface`
* (a public no-`any` facade) rather than the raw `SuperConverter`
* class. Header/footer code reads narrower-than-surface fields
* (`headers` / `footers` / `headerIds` / `footerIds` as the
* `HeaderFooterCollections` shape). Cast at the boundary instead of
* declaring an `interface … extends Editor` that overrides
* `converter` incompatibly with the surface.
*/
interface EditorWithConverter extends Editor {
type EditorWithConverter = Omit<Editor, 'converter'> & {
converter: HeaderFooterCollections;
}
};

export type HeaderFooterKind = 'header' | 'footer';
export type HeaderFooterVariant = (typeof HEADER_FOOTER_VARIANTS)[number];
Expand Down Expand Up @@ -184,12 +189,18 @@ export class HeaderFooterEditorManager extends EventEmitter {
}

/**
* Type guard to check if an editor has a converter property.
* Runtime check that the editor has a usable converter handle.
*
* SD-3240: cannot be a type predicate (`editor is EditorWithConverter`)
* because `Editor.converter` is `EditorConverterSurface` while
* `EditorWithConverter` overrides it to `HeaderFooterCollections`.
* The two shapes don't share a subtype relationship. Callers narrow
* with a local cast after the check.
*
* @param editor - The editor instance to check
* @returns True if the editor has a converter property
*/
#hasConverter(editor: Editor): editor is EditorWithConverter {
#hasConverter(editor: Editor): boolean {
return 'converter' in editor && editor.converter !== undefined && editor.converter !== null;
}

Expand Down Expand Up @@ -905,7 +916,7 @@ export class HeaderFooterEditorManager extends EventEmitter {
if (!this.#hasConverter(this.#editor)) {
return;
}
const converter = this.#editor.converter as Record<string, unknown>;
const converter = this.#editor.converter as unknown as Record<string, unknown>;
if (!converter) return;

const targetKey = descriptor.kind === 'header' ? 'headerEditors' : 'footerEditors';
Expand Down Expand Up @@ -940,7 +951,7 @@ export class HeaderFooterEditorManager extends EventEmitter {
if (!this.#hasConverter(this.#editor)) {
return;
}
const converter = this.#editor.converter as Record<string, unknown>;
const converter = this.#editor.converter as unknown as Record<string, unknown>;
if (!converter) return;

const targetKey = descriptor.kind === 'header' ? 'headerEditors' : 'footerEditors';
Expand Down Expand Up @@ -1329,7 +1340,7 @@ export class HeaderFooterLayoutAdapter {

const blockIdPrefix = `hf-${descriptor.kind}-${descriptor.id}-`;
const converterContext = this.#getConverterContext();
const rootConverter = (this.#manager.rootEditor as EditorWithConverter | undefined)?.converter as
const rootConverter = (this.#manager.rootEditor as unknown as EditorWithConverter | undefined)?.converter as
| { media?: Record<string, string>; getDocumentDefaultStyles?: () => { typeface?: string; fontSizePt?: number } }
| undefined;
const providedMedia = this.#mediaFiles;
Expand Down Expand Up @@ -1374,7 +1385,7 @@ export class HeaderFooterLayoutAdapter {
if (!('converter' in rootEditor)) {
return undefined;
}
const converter = (rootEditor as EditorWithConverter).converter as Record<string, unknown> | undefined;
const converter = (rootEditor as unknown as EditorWithConverter).converter as Record<string, unknown> | undefined;
if (!converter) return undefined;

const context: ConverterContext = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import { getMarksFromSelection } from './getMarksFromSelection.js';
import { findMark } from './findMark.js';

/**
* Result entry from `getActiveFormatting`. Discriminated union: the
* synthetic `copyFormat` flag uses `attrs: true`; every other entry
* carries a `Record<string, unknown>` attribute object.
*
* @typedef {{ name: 'copyFormat'; attrs: true }
* | { name: string; attrs: Record<string, unknown> }} ActiveFormattingEntry
*/

/**
* Narrow structural editor shape consumed by `getActiveFormatting`.
* Only `state` (PM EditorState) + `storage.formatCommands.storedStyle`
* are needed. Avoids resurfacing SD-3240 debt through full Editor.
*
* @typedef {{
* state: import('prosemirror-state').EditorState;
* storage: { formatCommands?: { storedStyle?: unknown } };
* }} ActiveFormattingEditorLike
*/

/**
* Compute the active formatting at the current selection. SD-3213 /
* SD-3245: typed signature replaces previous `(editor: any): any`.
*
* @param {ActiveFormattingEditorLike} editor
* @returns {ActiveFormattingEntry[]}
*/
export function getActiveFormatting(editor) {
const { state } = editor;
const { selection } = state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1267,7 +1267,9 @@ function copySequenceStateOverrides(editor, fromNumId, toNumId, levels) {
for (const sourceEl of sourceNumDef.elements) {
if (sourceEl.name !== 'w:lvlOverride') continue;

const ilvl = sourceEl.attributes?.['w:ilvl'];
// SD-3240: OOXML attribute values are typed as `unknown`; the
// level set expects strings, which is what the parser produces.
const ilvl = /** @type {string | null | undefined} */ (sourceEl.attributes?.['w:ilvl']);
if (ilvl == null) continue;
if (levelSet && !levelSet.has(ilvl)) continue;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,39 @@ import type { OrderedListStyle } from '../../../extensions/types/paragraph-comma
// Types
// ---------------------------------------------------------------------------

/**
* SD-3240: OOXML element subtree as held inside `NumberingModel`
* records. Recursive; each element has an optional name, attributes
* map, and nested children.
*/
export interface NumberingElement {
name?: string;
attributes?: Record<string, unknown>;
elements?: NumberingElement[];
[key: string]: unknown;
}

/**
* SD-3240: minimal shape internal callers read from numbering
* abstract / definition records. The OOXML element tree lives at
* `.elements`; specific tag-level fields are accessed via deeper
* indexing that callers narrow locally.
*/
export interface NumberingRecord {
name?: string;
attributes?: Record<string, unknown>;
elements?: NumberingElement[];
[key: string]: unknown;
}

export interface NumberingModel {
abstracts: Record<number, any>;
definitions: Record<number, any>;
// SD-3240: changed from `Record<number, any>` to a structural type
// with `.elements`. Internal callers read `.elements` to walk the
// OOXML tree; deeper field access goes through local casts. The
// change drains the audit findings reachable through
// `editor.converter.numbering.abstracts` / `.definitions`.
abstracts: Record<number, NumberingRecord>;
definitions: Record<number, NumberingRecord>;
}

interface GenerateOptions {
Expand Down Expand Up @@ -293,7 +323,10 @@ export function generateNewListDefinition(numbering: NumberingModel, options: Ge

if (level != null && start != null && text != null && fmt != null) {
if (numbering.definitions[numId]) {
const abstractId = numbering.definitions[numId]?.elements[0]?.attributes['w:val'];
// SD-3240: attribute values are typed as `unknown` (OOXML attrs
// can be any primitive). Cast to number for indexing into the
// typed `abstracts` map.
const abstractId = numbering.definitions[numId]?.elements?.[0]?.attributes?.['w:val'] as number;
newAbstractId = abstractId;
const abstract = numbering.abstracts[abstractId];
newAbstractDef = { ...abstract };
Expand Down Expand Up @@ -354,7 +387,9 @@ export function changeNumIdSameAbstract(
const newId = getNextId(numbering.definitions);

const def = numbering.definitions[numId];
const abstractId = def?.elements?.find((el: any) => el.name === 'w:abstractNumId')?.attributes?.['w:val'];
const abstractId = def?.elements?.find((el: NumberingElement) => el.name === 'w:abstractNumId')?.attributes?.[
'w:val'
] as number | undefined;
const abstract = abstractId != null ? numbering.abstracts[abstractId] : undefined;

if (!abstract) {
Expand Down Expand Up @@ -386,7 +421,7 @@ export function removeListDefinitions(numbering: NumberingModel, listId: number)
const def = numbering.definitions[listId];
if (!def) return;

const abstractId = def.elements?.[0]?.attributes?.['w:val'];
const abstractId = def.elements?.[0]?.attributes?.['w:val'] as number | undefined;
delete numbering.definitions[listId];
if (abstractId != null) delete numbering.abstracts[abstractId];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6116,15 +6116,20 @@ export class PresentationEditor extends EventEmitter {
}
}

// SD-3240: converter.convertedXml / translatedLinkedStyles /
// translatedNumbering are typed on the public surface as
// narrower (unknown-bearing) shapes than ConverterContext
// requires. Cast at the boundary; the runtime values match
// the shape ConverterContext expects.
converterContext = converter
? {
? ({
docx: converter.convertedXml,
...(Object.keys(footnoteNumberById).length ? { footnoteNumberById } : {}),
...(Object.keys(endnoteNumberById).length ? { endnoteNumberById } : {}),
translatedLinkedStyles: converter.translatedLinkedStyles,
translatedNumbering: converter.translatedNumbering,
...(defaultTableStyleId ? { defaultTableStyleId } : {}),
}
} as unknown as ConverterContext)
: undefined;
const atomNodeTypes = getAtomNodeTypesFromSchema(this.#editor?.schema ?? null);
const positionMapStart = perfNow();
Expand All @@ -6143,7 +6148,10 @@ export class PresentationEditor extends EventEmitter {
enableTrackedChanges: this.#trackedChangesEnabled,
enableComments: commentsEnabled,
enableRichHyperlinks: true,
themeColors: this.#editor?.converter?.themeColors ?? undefined,
// SD-3240: converter.themeColors is `unknown` on the public
// EditorConverterSurface; cast to the consumer-expected type
// here. The runtime shape matches at call time.
themeColors: (this.#editor?.converter?.themeColors ?? undefined) as Record<string, string> | undefined,
converterContext,
flowBlockCache: this.#flowBlockCache,
showBookmarks: this.#layoutOptions.showBookmarks ?? false,
Expand Down
Loading
Loading