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
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 @@ -176,6 +176,8 @@ export function exportSchemaToJson(params) {
commentRangeEnd: wCommentRangeEndTranslator,
permStart: wPermStartTranslator,
permEnd: wPermEndTranslator,
permStartBlock: wPermStartTranslator,
permEndBlock: wPermEndTranslator,
commentReference: () => null,
footnoteReference: wFootnoteReferenceTranslator,
shapeContainer: pictTranslator,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,8 @@ export function filterOutRootInlineNodes(content = []) {
'commentReference',
'footnoteReference',
'structuredContent',
'permStart',
'permEnd',
]);

const PRESERVABLE_INLINE_XML_NAMES = {
Expand All @@ -828,6 +830,14 @@ export function filterOutRootInlineNodes(content = []) {
return;
}

if (type === 'permStart' || type === 'permEnd') {
result.push({
...node,
type: type === 'permStart' ? 'permStartBlock' : 'permEndBlock',
});
return;
}

if (!INLINE_TYPES.has(type)) {
result.push(node);
} else if (preservableNodeName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ describe('filterOutRootInlineNodes', () => {
expect(result[1].type).toBe('paragraph');
});

it('converts root permission tags into block nodes', () => {
const input = [n('permStart', { id: '1' }), n('permEnd', { id: '1' })];
const result = filterOutRootInlineNodes(input);
expect(result.map((node) => node.type)).toEqual(['permStartBlock', 'permEndBlock']);
expect(result[0].attrs.id).toBe('1');
expect(result[1].attrs.id).toBe('1');
});

it('derives inline types from schema when provided', () => {
// Build a minimal fake schema map using Map with forEach(name, nodeType)
const nodes = new Map();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,8 @@
import { carbonCopy } from '@core/utilities/carbonCopy.js';
import { registeredHandlers } from '../../v3/handlers/index.js';
import { isInlineContext } from '@core/super-converter/utils/inlineContext.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;
};
export { isInlineContext };

/**
* @type {import('docxImporter').NodeHandler}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import { handlePermStartNode } from './permStartImporter.js';
import { handlePermEndNode } from './permEndImporter.js';

const createParams = (node, extra = {}) => ({
nodes: [node],
docx: {},
nodeListHandler: { handler: () => [], handlerEntities: [] },
...extra,
});

describe('permission range importers', () => {
it('creates block-level permStart when not in inline context', () => {
const node = { name: 'w:permStart', attributes: { 'w:id': '1' } };
const { nodes, consumed } = handlePermStartNode(createParams(node, { path: [{ name: 'w:body' }] }));
expect(consumed).toBe(1);
expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe('permStartBlock');
expect(nodes[0].attrs.id).toBe('1');
});

it('creates block-level permEnd when not in inline context', () => {
const node = { name: 'w:permEnd', attributes: { 'w:id': '2', 'w:displacedByCustomXml': 'prev' } };
const { nodes, consumed } = handlePermEndNode(createParams(node, { path: [{ name: 'w:body' }] }));
expect(consumed).toBe(1);
expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe('permEndBlock');
expect(nodes[0].attrs.id).toBe('2');
});
});
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
// @ts-check
import { NodeTranslator } from '@translator';
import { isInlineContext } from '@core/super-converter/utils/inlineContext.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_NAME = ['permEnd', 'permEndBlock'];

/**
* Encode a <w:permEnd> node as a SuperDoc permEnd node.
* Encode a <w:permEnd> node as a SuperDoc permEnd/permEndBlock node.
* @param {import('@translator').SCEncoderConfig} params
* @param {import('@translator').EncodedAttributes} [encodedAttrs]
* @returns {import('@translator').SCEncoderResult}
*/
const encode = (params, encodedAttrs = {}) => {
const node = params?.nodes?.[0];
const isInline = isInlineContext(params?.path || [], node?.name);
return {
type: 'permEnd',
type: isInline ? 'permEnd' : 'permEndBlock',
attrs: encodedAttrs,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ 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);
});

it('encodes OOXML to SuperDoc', () => {
it('encodes OOXML to SuperDoc inline', () => {
const params = {
nodes: [
{
Expand All @@ -21,6 +21,7 @@ describe('w:permEnd translator', () => {
},
},
],
path: [{ name: 'w:p' }],
};

const result = translator.encode(params);
Expand All @@ -34,6 +35,31 @@ describe('w:permEnd translator', () => {
});
});

it('encodes block-level nodes when not in inline context', () => {
const params = {
nodes: [
{
name: 'w:permEnd',
attributes: {
'w:id': '7',
'w:displacedByCustomXml': 'prev',
},
},
],
path: [],
};

const result = translator.encode(params);

expect(result).toEqual({
type: 'permEndBlock',
attrs: {
id: '7',
displacedByCustomXml: 'prev',
},
});
});

it('decodes SuperDoc to OOXML', () => {
const params = {
node: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
// @ts-check
import { NodeTranslator } from '@translator';
import { isInlineContext } from '@core/super-converter/utils/inlineContext.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_NAME = ['permStart', 'permStartBlock'];

/**
* Encode a <w:permStart> node as a SuperDoc permStart node.
* Encode a <w:permStart> node as a SuperDoc permStart/permStartBlock node.
* @param {import('@translator').SCEncoderConfig} params
* @param {import('@translator').EncodedAttributes} [encodedAttrs]
* @returns {import('@translator').SCEncoderResult}
*/
const encode = (params, encodedAttrs = {}) => {
const node = params?.nodes?.[0];
const isInline = isInlineContext(params?.path || [], node?.name);
return {
type: 'permStart',
type: isInline ? 'permStart' : 'permStartBlock',
attrs: encodedAttrs,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ 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);
});

it('encodes OOXML to SuperDoc with all attributes', () => {
it('encodes OOXML to SuperDoc inline with all attributes', () => {
const params = {
nodes: [
{
Expand All @@ -24,6 +24,7 @@ describe('w:permStart translator', () => {
},
},
],
path: [{ name: 'w:p' }],
};

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

it('encodes with minimal attributes', () => {
it('encodes inline with minimal attributes', () => {
const params = {
nodes: [
{
Expand All @@ -50,6 +51,7 @@ describe('w:permStart translator', () => {
},
},
],
path: [{ name: 'w:p' }],
};

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

it('encodes block-level nodes when not in inline context', () => {
const params = {
nodes: [
{
name: 'w:permStart',
attributes: {
'w:id': '21',
},
},
],
path: [],
};

const result = translator.encode(params);

expect(result).toEqual({
type: 'permStartBlock',
attrs: {
id: '21',
},
});
});

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 @@ -73,8 +73,8 @@ import { PermissionRanges } from './permission-ranges/index.js';
import { VerticalNavigation } from './vertical-navigation/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 @@ -194,6 +194,8 @@ const getStarterExtensions = () => {
ShapeGroup,
PermStart,
PermEnd,
PermStartBlock,
PermEndBlock,
PermissionRanges,
VerticalNavigation,
PassthroughInline,
Expand Down
Loading
Loading