Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/super-editor/src/core/super-converter/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ export function exportSchemaToJson(params) {
commentRangeStart: wCommentRangeStartTranslator,
commentRangeEnd: wCommentRangeEndTranslator,
permStart: wPermStartTranslator,
permStartBlock: wPermStartTranslator,
permEnd: wPermEndTranslator,
permEndBlock: wPermEndTranslator,
commentReference: () => null,
footnoteReference: wFootnoteReferenceTranslator,
shapeContainer: pictTranslator,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const INLINE_PARENT_NAMES = new Set([
'w:r',
'w:hyperlink',
'w:smartTag',
'w:fldSimple',
'w:proofErr',
'w:del',
'w:ins',
'w:p', // Paragraph is an inline container; unknown children must be inline-safe
]);

const INLINE_NODE_NAMES = new Set([
'm:oMathPara',
'm:oMath',
'm:t',
'm:r',
'm:ctrlPr',
'm:sSupPr',
'm:e',
'm:sup',
'm:sSup',
]);

const BLOCK_BOUNDARY_NAMES = new Set(['w:body', 'w:tbl', 'w:tc', 'w:tr']);

/**
* Determines if the current DOCX node position accepts inline-only content.
* Walks up the ancestor chain until it finds an inline parent or block boundary.
*
* @param {Array<{ name?: string }>} [path=[]] Ancestor chain leading to the current node.
* @param {string} [currentNodeName] Optional immediate node name override.
* @returns {boolean}
*/
export const isInlineContext = (path = [], currentNodeName) => {
const immediateName = currentNodeName ?? path[path.length - 1]?.name;
if (immediateName && INLINE_NODE_NAMES.has(immediateName)) {
return true;
}
if (!Array.isArray(path) || path.length === 0) return false;

for (let i = path.length - 1; i >= 0; i--) {
const ancestorName = path[i]?.name;
if (!ancestorName) continue;
if (INLINE_NODE_NAMES.has(ancestorName) || INLINE_PARENT_NAMES.has(ancestorName)) {
return true;
}
if (BLOCK_BOUNDARY_NAMES.has(ancestorName)) {
return false;
}
}

return false;
};
Comment on lines +1 to +53
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just moved this out of passthroughNodeImporter so it can be reused.

Original file line number Diff line number Diff line change
@@ -1,50 +1,7 @@
import { carbonCopy } from '@core/utilities/carbonCopy.js';
import { isInlineContext } from '../../helpers/node-context.js';
import { registeredHandlers } from '../../v3/handlers/index.js';

const INLINE_PARENT_NAMES = new Set([
'w:r',
'w:hyperlink',
'w:smartTag',
'w:fldSimple',
'w:proofErr',
'w:del',
'w:ins',
'w:p', // Paragraph is an inline container; unknown children must be inline-safe
]);
const INLINE_NODE_NAMES = new Set([
'm:oMathPara',
'm:oMath',
'm:t',
'm:r',
'm:ctrlPr',
'm:sSupPr',
'm:e',
'm:sup',
'm:sSup',
]);
const BLOCK_BOUNDARY_NAMES = new Set(['w:body', 'w:tbl', 'w:tc', 'w:tr']);

export const isInlineContext = (path = [], currentNodeName) => {
const immediateName = currentNodeName ?? path[path.length - 1]?.name;
if (immediateName && INLINE_NODE_NAMES.has(immediateName)) {
return true;
}
if (!Array.isArray(path) || path.length === 0) return false;

for (let i = path.length - 1; i >= 0; i--) {
const ancestorName = path[i]?.name;
if (!ancestorName) continue;
if (INLINE_NODE_NAMES.has(ancestorName) || INLINE_PARENT_NAMES.has(ancestorName)) {
return true;
}
if (BLOCK_BOUNDARY_NAMES.has(ancestorName)) {
return false;
}
}

return false;
};

/**
* @type {import('docxImporter').NodeHandler}
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import { handlePassthroughNode, isInlineContext } from './passthroughNodeImporter.js';
import { handlePassthroughNode } from './passthroughNodeImporter.js';
import { isInlineContext } from '../../helpers/node-context.js';

const createParams = (node, extra = {}) => ({
nodes: [node],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
// @ts-check
import { NodeTranslator } from '@translator';
import { isInlineContext } from '../../../../helpers/node-context.js';
import validXmlAttributes from './attributes/index.js';

/** @type {import('@translator').XmlNodeName} */
const XML_NODE_NAME = 'w:permEnd';

/** @type {import('@translator').SuperDocNodeOrKeyName} */
const SD_NODE_NAME = 'permEnd';
const SD_NODE_NAMES = /** @type {const} */ (['permEnd', 'permEndBlock']);

const INLINE_NODE_NAME = SD_NODE_NAMES[0];
const BLOCK_NODE_NAME = SD_NODE_NAMES[1];

const resolveNodeType = (params) => {
const inlineContext = isInlineContext(params?.path);
return inlineContext ? INLINE_NODE_NAME : BLOCK_NODE_NAME;
};

/**
* Encode a <w:permEnd> node as a SuperDoc permEnd node.
* @param {import('@translator').SCEncoderConfig} params
* @param {import('@translator').EncodedAttributes} [encodedAttrs]
* @returns {import('@translator').SCEncoderResult}
*/
const encode = (params, encodedAttrs = {}) => {
return {
type: 'permEnd',
attrs: encodedAttrs,
};
};
const encode = (params, encodedAttrs = {}) => ({
type: resolveNodeType(params),
attrs: encodedAttrs,
});

/**
* Decode a SuperDoc permEnd node back into OOXML <w:permEnd>.
Expand All @@ -43,7 +50,7 @@ const decode = (params, decodedAttrs = {}) => {
/** @type {import('@translator').NodeTranslatorConfig} */
export const config = {
xmlName: XML_NODE_NAME,
sdNodeOrKeyName: SD_NODE_NAME,
sdNodeOrKeyName: SD_NODE_NAMES,
type: NodeTranslator.translatorTypes.NODE,
encode,
decode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NodeTranslator } from '@translator';
describe('w:permEnd translator', () => {
it('exposes correct config', () => {
expect(config.xmlName).toBe('w:permEnd');
expect(config.sdNodeOrKeyName).toBe('permEnd');
expect(config.sdNodeOrKeyName).toEqual(['permEnd', 'permEndBlock']);
expect(config.type).toBe(NodeTranslator.translatorTypes.NODE);
expect(config.attributes).toHaveLength(2);
});
Expand All @@ -21,6 +21,7 @@ describe('w:permEnd translator', () => {
},
},
],
path: [{ name: 'w:p' }],
};

const result = translator.encode(params);
Expand Down Expand Up @@ -72,4 +73,38 @@ describe('w:permEnd translator', () => {

expect(decoded).toEqual(original);
});

it('creates inline permEnd nodes inside inline contexts', () => {
const params = {
nodes: [
{
name: 'w:permEnd',
attributes: {
'w:id': 'inline',
},
},
],
path: [{ name: 'w:p' }],
};

const result = translator.encode(params);
expect(result.type).toBe('permEnd');
});

it('creates block permEnd nodes when parent is block-level', () => {
const params = {
nodes: [
{
name: 'w:permEnd',
attributes: {
'w:id': 'block',
},
},
],
path: [{ name: 'w:body' }],
};

const result = translator.encode(params);
expect(result.type).toBe('permEndBlock');
});
});
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
// @ts-check
import { NodeTranslator } from '@translator';
import { isInlineContext } from '../../../../helpers/node-context.js';
import validXmlAttributes from './attributes/index.js';

/** @type {import('@translator').XmlNodeName} */
const XML_NODE_NAME = 'w:permStart';

/** @type {import('@translator').SuperDocNodeOrKeyName} */
const SD_NODE_NAME = 'permStart';
const SD_NODE_NAMES = /** @type {const} */ (['permStart', 'permStartBlock']);

const INLINE_NODE_NAME = SD_NODE_NAMES[0];
const BLOCK_NODE_NAME = SD_NODE_NAMES[1];

const resolveNodeType = (params) => {
const inlineContext = isInlineContext(params?.path);
return inlineContext ? INLINE_NODE_NAME : BLOCK_NODE_NAME;
};

/**
* Encode a <w:permStart> node as a SuperDoc permStart node.
* @param {import('@translator').SCEncoderConfig} params
* @param {import('@translator').EncodedAttributes} [encodedAttrs]
* @returns {import('@translator').SCEncoderResult}
*/
const encode = (params, encodedAttrs = {}) => {
return {
type: 'permStart',
attrs: encodedAttrs,
};
};
const encode = (params, encodedAttrs = {}) => ({
type: resolveNodeType(params),
attrs: encodedAttrs,
});

/**
* Decode a SuperDoc permStart node back into OOXML <w:permStart>.
Expand All @@ -43,7 +50,7 @@ const decode = (params, decodedAttrs = {}) => {
/** @type {import('@translator').NodeTranslatorConfig} */
export const config = {
xmlName: XML_NODE_NAME,
sdNodeOrKeyName: SD_NODE_NAME,
sdNodeOrKeyName: SD_NODE_NAMES,
type: NodeTranslator.translatorTypes.NODE,
encode,
decode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NodeTranslator } from '@translator';
describe('w:permStart translator', () => {
it('exposes correct config', () => {
expect(config.xmlName).toBe('w:permStart');
expect(config.sdNodeOrKeyName).toBe('permStart');
expect(config.sdNodeOrKeyName).toEqual(['permStart', 'permStartBlock']);
expect(config.type).toBe(NodeTranslator.translatorTypes.NODE);
expect(config.attributes).toHaveLength(5);
});
Expand All @@ -24,6 +24,7 @@ describe('w:permStart translator', () => {
},
},
],
path: [{ name: 'w:p' }],
};

const result = translator.encode(params);
Expand All @@ -50,6 +51,7 @@ describe('w:permStart translator', () => {
},
},
],
path: [{ name: 'w:p' }],
};

const result = translator.encode(params);
Expand All @@ -62,6 +64,40 @@ describe('w:permStart translator', () => {
});
});

it('creates inline nodes when context allows inline content', () => {
const params = {
nodes: [
{
name: 'w:permStart',
attributes: {
'w:id': 'inline',
},
},
],
path: [{ name: 'w:p' }],
};

const result = translator.encode(params);
expect(result.type).toBe('permStart');
});

it('creates block nodes when context requires block content', () => {
const params = {
nodes: [
{
name: 'w:permStart',
attributes: {
'w:id': 'block',
},
},
],
path: [{ name: 'w:body' }],
};

const result = translator.encode(params);
expect(result.type).toBe('permStartBlock');
});

it('decodes SuperDoc to OOXML', () => {
const params = {
node: {
Expand Down
6 changes: 4 additions & 2 deletions packages/super-editor/src/extensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ import { CustomSelection } from './custom-selection/index.js';
import { PermissionRanges } from './permission-ranges/index.js';

// Permissions
import { PermStart } from './perm-start/index.js';
import { PermEnd } from './perm-end/index.js';
import { PermStart, PermStartBlock } from './perm-start/index.js';
import { PermEnd, PermEndBlock } from './perm-end/index.js';

// Helpers
import { trackChangesHelpers } from './track-changes/index.js';
Expand Down Expand Up @@ -189,6 +189,8 @@ const getStarterExtensions = () => {
ShapeGroup,
PermStart,
PermEnd,
PermStartBlock,
PermEndBlock,
PermissionRanges,
PassthroughInline,
PassthroughBlock,
Expand Down
Loading
Loading