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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ and this project adheres to
### Added

- ✨(backend) support creating subdoc from file #1987
- ✨(frontend) justified text alignment in the formatting toolbar #2294
- ✨(buildpack) add PaaS deployment support, tested with Scalingo #2293
- 🔧(backend) allow configuring settings OIDC_OP_USER_ENDPOINT_FORMAT

### Fixed

- 🐛(frontend) DOCX export helpers map justified text to OOXML `both` for blocks using app-provided paragraph mappings (e.g. callout, quote, image caption)
- 🐛(docs) run migration 0027 without superuser role


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
FormattingToolbar,
FormattingToolbarController,
TextAlignButton,
blockTypeSelectItems,
getFormattingToolbarItems,
useDictionary,
Expand Down Expand Up @@ -74,6 +75,25 @@ export const BlockNoteToolbar = ({ aiAllowed }: { aiAllowed: boolean }) => {
return true;
});

const rightAlignIndex = toolbarItems.findIndex(
(item) =>
typeof item === 'object' &&
item !== null &&
'key' in item &&
item.key === 'textAlignRightButton',
);

if (rightAlignIndex !== -1) {
toolbarItems.splice(
rightAlignIndex + 1,
0,
<TextAlignButton
key="textAlignJustifyButton"
textAlignment="justify"
/>,
);
}

return toolbarItems;
}, [dict, t]);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
COLORS_DEFAULT,
DefaultProps,
UnreachableCaseError,
} from '@blocknote/core';
import { IParagraphOptions, ShadingType } from 'docx';

/**
* Same semantics as `@blocknote/xl-docx-exporter` `blockPropsToStyles`, but
* `textAlignment: justify` maps to OOXML `both` (normal justified paragraphs).
* Upstream maps justify to `distribute`, which uses “distribute all characters equally”
* and does not match browser/Word paragraph justification.
*/
export function blockNoteDocxBlockPropsToStyles(
props: Partial<DefaultProps>,
colors: typeof COLORS_DEFAULT,
): IParagraphOptions {
return {
shading:
props.backgroundColor === 'default' || !props.backgroundColor
? undefined
: {
type: ShadingType.CLEAR,
fill: (() => {
const color = colors[props.backgroundColor]?.background;
if (!color) {
return undefined;
}
return color.slice(1);
})(),
},
run:
props.textColor === 'default' || !props.textColor
? undefined
: {
color: (() => {
const color = colors[props.textColor]?.text;
if (!color) {
return undefined;
}
return color.slice(1);
})(),
},
Comment on lines +19 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In the docxnpm library, areParagraphoption fields likeshading.fillandrun.colorallowed to beundefined, or should the entire shading/run object be omitted when color is unresolved?

💡 Result:

Yes—within docx, option fields such as shading.fill and run.color are defined as optional, and passing them as undefined (i.e., leaving them undefined) is supported; docx omits the corresponding XML attribute when the value is undefined. Therefore, when a color is unresolved, you can set the individual option fields to undefined rather than needing to omit the entire shading/run object. In docx’s shading implementation, the IShadingAttributesProperties type defines fill/color/type as optional fields, and createShading maps undefined to “no attribute” by setting the XML attribute value to undefined when fill/color are undefined [1]. This means the library naturally supports “unresolved” colors by omitting only those attributes, not the entire parent object. Similarly for run-level color: docx’s public type definitions for run properties expose color?: string (optional) [2], meaning you should provide it only when you have a concrete value. If you omit/leave it undefined, the serialization layer can exclude it (and RunProperties.prepForXml is designed to exclude empty components from XML serialization) [3]. Practical guidance: - If you have a Paragraph/Run options object and you don’t know the color yet, omit the specific fields (or leave them as undefined). - Only omit the entire shading or run object if the whole component would be empty/unneeded for your desired formatting. Answering your exact question: you do not need to omit the entire shading/run object when shading.fill or run.color is unresolved; docx is designed to allow those individual fields to be undefined and will omit the underlying XML attributes [1][2].

Citations:


DOCX accepts undefined style fields; omitting the whole shading/run object isn’t required

docx’s types/serialization support optional style attributes like shading.fill and run.color; leaving those fields undefined simply omits the corresponding XML attributes. You can still choose to omit the entire shading/run block when the lookup fails to reduce emitted XML, but the current undefined fill/color values shouldn’t cause invalid output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/blockNoteDocxBlockProps.ts`
around lines 19 - 43, The current code conditionally omits the entire
shading/run objects when backgroundColor/textColor are 'default' or missing, but
docx can accept undefined subfields; simplify by always returning the shading
object (with type: ShadingType.CLEAR) and the run object while setting
shading.fill to colors[props.backgroundColor]?.background?.slice(1) (or
undefined) and run.color to colors[props.textColor]?.text?.slice(1) (or
undefined), removing the outer ternaries that produce undefined for the whole
shading/run so invalid XML won’t be produced and lookups that miss a color
simply leave the subfield undefined; update the code around the shading and run
expressions (references: shading, ShadingType.CLEAR, run, colors,
props.backgroundColor, props.textColor) accordingly.

alignment:
!props.textAlignment || props.textAlignment === 'left'
? undefined
: props.textAlignment === 'center'
? 'center'
: props.textAlignment === 'right'
? 'right'
: props.textAlignment === 'justify'
? 'both'
: (() => {
throw new UnreachableCaseError(props.textAlignment);
})(),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { CheckBox, Paragraph, TextRun } from 'docx';

import { DocsExporterDocx } from '../types';

import { blockNoteDocxBlockPropsToStyles } from './blockNoteDocxBlockProps';

/** Default-schema DOCX blocks with correct `justify` → `both` (see blockNoteDocxBlockProps). */

export const blockMappingParagraphDocxJustifyBoth: DocsExporterDocx['mappings']['blockMapping']['paragraph'] =
(block, exporter) =>
new Paragraph({
...blockNoteDocxBlockPropsToStyles(block.props, exporter.options.colors),
children: exporter.transformInlineContent(block.content),
});

export const blockMappingHeadingDocxJustifyBoth: DocsExporterDocx['mappings']['blockMapping']['heading'] =
(block, exporter) =>
new Paragraph({
...blockNoteDocxBlockPropsToStyles(block.props, exporter.options.colors),
children: exporter.transformInlineContent(block.content),
heading: `Heading${block.props.level as 1 | 2 | 3 | 4 | 5 | 6}`,
});

export const blockMappingBulletListItemDocxJustifyBoth: DocsExporterDocx['mappings']['blockMapping']['bulletListItem'] =
(block, exporter, nestingLevel) =>
new Paragraph({
...blockNoteDocxBlockPropsToStyles(block.props, exporter.options.colors),
children: exporter.transformInlineContent(block.content),
numbering: {
reference: 'blocknote-bullet-list',
level: nestingLevel,
},
});

export const blockMappingNumberedListItemDocxJustifyBoth: DocsExporterDocx['mappings']['blockMapping']['numberedListItem'] =
(block, exporter, nestingLevel) =>
new Paragraph({
...blockNoteDocxBlockPropsToStyles(block.props, exporter.options.colors),
children: exporter.transformInlineContent(block.content),
numbering: {
reference: 'blocknote-numbered-list',
level: nestingLevel,
},
});

export const blockMappingToggleListItemDocxJustifyBoth: DocsExporterDocx['mappings']['blockMapping']['toggleListItem'] =
(block, exporter) =>
new Paragraph({
...blockNoteDocxBlockPropsToStyles(block.props, exporter.options.colors),
children: [
new TextRun({
children: ['> '],
}),
...exporter.transformInlineContent(block.content),
],
});

export const blockMappingCheckListItemDocxJustifyBoth: DocsExporterDocx['mappings']['blockMapping']['checkListItem'] =
(block, exporter) =>
new Paragraph({
...blockNoteDocxBlockPropsToStyles(block.props, exporter.options.colors),
children: [
new CheckBox({ checked: block.props.checked }),
new TextRun({
children: [' '],
}),
...exporter.transformInlineContent(block.content),
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function blockPropsToStyles(
: props.textAlignment === 'right'
? 'right'
: props.textAlignment === 'justify'
? 'distribute'
? 'both'
: (() => {
throw new UnreachableCaseError(props.textAlignment);
})(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './blockNoteDocxBlockProps';
export * from './calloutDocx';
export * from './defaultBlocksDocxJustify';
export * from './calloutODT';
export * from './calloutPDF';
export * from './headingPDF';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';

import {
blockMappingBulletListItemDocxJustifyBoth,
blockMappingCalloutDocx,
blockMappingCheckListItemDocxJustifyBoth,
blockMappingHeadingDocxJustifyBoth,
blockMappingImageDocx,
blockMappingNumberedListItemDocxJustifyBoth,
blockMappingParagraphDocxJustifyBoth,
blockMappingQuoteDocx,
blockMappingToggleListItemDocxJustifyBoth,
blockMappingUploadLoaderDocx,
} from './blocks-mapping';
import { inlineContentMappingInterlinkingLinkDocx } from './inline-content-mapping';
Expand All @@ -13,6 +19,12 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
...docxDefaultSchemaMappings,
blockMapping: {
...docxDefaultSchemaMappings.blockMapping,
paragraph: blockMappingParagraphDocxJustifyBoth,
heading: blockMappingHeadingDocxJustifyBoth,
bulletListItem: blockMappingBulletListItemDocxJustifyBoth,
numberedListItem: blockMappingNumberedListItemDocxJustifyBoth,
toggleListItem: blockMappingToggleListItemDocxJustifyBoth,
checkListItem: blockMappingCheckListItemDocxJustifyBoth,
callout: blockMappingCalloutDocx,
// We're reusing the file block mapping for PDF blocks; both share the same
// implementation signature, so we can reuse the handler directly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function docxBlockPropsToStyles(
: props.textAlignment === 'right'
? 'right'
: props.textAlignment === 'justify'
? 'distribute'
? 'both'
: (() => {
throw new UnreachableCaseError(props.textAlignment);
})(),
Expand Down