8000 DEV-24325 Add support for comment replies by harvestcore · Pull Request #21 · FontoXML/docxml · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

DEV-24325 Add support for comment replies #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 1, 2025
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: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fontoxml/docxml",
"version": "5.15.8",
"version": "5.15.8-a",
"description": "A module for making .docx documents from scratch or from an existing .docx or .dotx template.",
"license": "MIT",
"homepage": "https://jsr.io/@fontoxml/docxml",
Expand Down
49 changes: 43 additions & 6 deletions examples/comments.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,61 @@
/** @jsx Docx.jsx */
import Docx, { Comment, CommentRangeEnd, CommentRangeStart, Paragraph } from '../mod.ts';
import Docx, {
Comment,
CommentRangeEnd,
CommentRangeStart,
Paragraph,
} from '../mod.ts';

const docx = Docx.fromNothing();

docx.document.styles.add({
id: 'CommentReference',
type: 'character',
paragraph: {},
});

const comment = docx.document.comments.add(
{
author: 'Wybe',
date: new Date(),
initials: 'X',
},
<Paragraph>According to some</Paragraph>,
<Paragraph>According to some</Paragraph>
);

const reply = docx.document.comments.add(
{
author: 'Angel',
date: new Date(),
parentId: comment,
},
<Paragraph>I agree!</Paragraph>
);

const reply2 = docx.document.comments.add(
{
author: 'Roy',
date: new Date(),
parentId: comment,
},
<Paragraph>I don't!</Paragraph>
);

docx.document.set(
<Paragraph>
NSYNC is the <CommentRangeStart id={comment} />
NSYNC is the
<CommentRangeStart id={comment} />
<CommentRangeStart id={reply} />
<CommentRangeStart id={reply2} />
greatest
<Comment id={comment} />
grea test
<CommentRangeEnd id={comment} /> band in history.
</Paragraph>,
<Comment id={reply} />
<Comment id={reply2} />
<CommentRangeEnd id={comment} />
<CommentRangeEnd id={reply} />
<CommentRangeEnd id={reply2} />
band in history.
</Paragraph>
);

await docx.toFile('comments.docx');
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export {

// Utility functions
export { RelationshipType } from './src/enums.ts';
export { hex, int, type Id } from './src/utilities/id.ts';
export { jsx } from './src/utilities/jsx.ts';
export {
cm,
Expand Down
23 changes: 12 additions & 11 deletions src/components/Comment.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type ComponentAncestor, Component } from '../classes/Component.ts';
import { DocumentXml } from '../files/DocumentXml.ts';
import { registerComponent } from '../utilities/components.ts';
import { create } from '../utilities/dom.ts';
import { type Id, int } from '../utilities/id.ts';
import { QNS } from '../utilities/namespaces.ts';
import { evaluateXPathToMap } from '../utilities/xquery.ts';

Expand All @@ -14,7 +15,7 @@ export type CommentChild = never;
* A type describing the props accepted by {@link Comment}.
*/
export type CommentProps = {
id: number;
id: Id;
};

/**
Expand All @@ -30,10 +31,11 @@ export class Comment extends Component<CommentProps, CommentChild> {
*/
public override toNode(ancestry: ComponentAncestor[]): Node {
const doc = ancestry.find(
(ancestor): ancestor is DocumentXml => ancestor instanceof DocumentXml,
(ancestor): ancestor is DocumentXml =>
ancestor instanceof DocumentXml
);
if (!doc || !doc.comments.has(this.props.id)) {
throw new Error(`Comment "${this.props.id}" does not exist`);
if (!doc || !doc.comments.has(this.props.id.int)) {
throw new Error(`Comment "${this.props.id.int}" does not exist`);
}
return create(
`
Expand All @@ -49,8 +51,8 @@ export class Comment extends Component<CommentProps, CommentChild> {
}
`,
{
id: this.props.id,
},
id: this.props.id.int,
}
);
}

Expand All @@ -65,16 +67,15 @@ export class Comment extends Component<CommentProps, CommentChild> {
* Instantiate this component from the XML in an existing DOCX file.
*/
static override fromNode(node: Node): Comment {
return new Comment(
evaluateXPathToMap<CommentProps>(
`
const { id } = evaluateXPathToMap<{ id: number }>(
`
map {
"id": ./@${QNS.w}id/number()
}
`,
node,
),
node
);
return new Comment({ id: int(id) });
}
}

Expand Down
28 changes: 15 additions & 13 deletions src/components/Paragraph.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'std/expect';
import { describe, it } from 'std/testing/bdd';
import { expect } from 'std/expect';
import { describe, it } from 'std/testing/bdd';

import { Archive } from '../classes/Archive.ts';
import type { ComponentContext } from '../classes/Component.ts';
Expand Down Expand Up @@ -30,7 +30,7 @@ describe('Paragraph from XML', () => {
</w:r>
</w:p>
`),
emptyContext,
emptyContext
);

it('parses props correctly', () => {
Expand All @@ -45,21 +45,21 @@ describe('Paragraph from XML', () => {
it('serializes correctly', async () => {
expect(serialize(await paragraph.toNode([]))).toBe(
`
<p xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<p xmlns="${NamespaceUri.w}" xmlns:ns1="${NamespaceUri.w14}" ns1:paraId="4CE0D358">
<pPr>
<pStyle xmlns:ns1="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ns1:val="Header"/>
<pStyle xmlns:ns2="${NamespaceUri.w}" ns2:val="Header"/>
<rPr>
<lang xmlns:ns2="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ns2:val="en-GB"/>
<lang xmlns:ns3="${NamespaceUri.w}" ns3:val="en-GB"/>
</rPr>
</pPr>
<r>
<rPr>
<lang xmlns:ns3="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ns3:val="nl-NL"/>
<lang xmlns:ns4="${NamespaceUri.w}" ns4:val="nl-NL"/>
</rPr>
<t xml:space="preserve">My custom template</t>
</r>
</p>
`.replace(/\n|\t/g, ''),
`.replace(/\n|\t/g, '')
);
});
});
Expand All @@ -78,17 +78,19 @@ describe('Paragraph with style change', () => {
it('serializes correctly', async () => {
expect(serialize(await paragraph.toNode([]))).toBe(
`
<p xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<p xmlns="${NamespaceUri.w}">
<pPr>
<pStyle xmlns:ns1="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ns1:val="StyleNew"/>
<pPrChange xmlns:ns2="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ns2:id="0" ns2:author="Wybe" ns2:date="${now.toISOString()}">
<pStyle xmlns:ns1="${NamespaceUri.w}" ns1:val="StyleNew"/>
<pPrChange xmlns:ns2="${
NamespaceUri.w
}" ns2:id="0" ns2:author="Wybe" ns2:date="${now.toISOString()}">
<pPr>
<pStyle ns2:val="StyleOld"/>
</pPr>
</pPrChange>
</pPr>
</p>
`.replace(/\n|\t/g, ''),
`.replace(/\n|\t/g, '')
);
});
});
});
59 changes: 51 additions & 8 deletions src/components/Paragraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,23 @@ import './TextAddition.ts';
import './TextDeletion.ts';

import type { Hyperlink } from '../../mod.ts';
import { type ComponentAncestor, Component, type ComponentContext } from '../classes/Component.ts';
import {
Component,
type ComponentAncestor,
type ComponentContext,
} from '../classes/Component.ts';
import type { ParagraphProperties } from '../properties/paragraph-properties.ts';
import {
paragraphPropertiesFromNode,
paragraphPropertiesToNode,
} from '../properties/paragraph-properties.ts';
import type { SectionProperties } from '../properties/section-properties.ts';
import { createChildComponentsFromNodes, registerComponent } from '../utilities/components.ts';
import {
createChildComponentsFromNodes,
registerComponent,
} from '../utilities/components.ts';
import { create } from '../utilities/dom.ts';
import { hex, type Id } from '../utilities/id.ts';
import { QNS } from '../utilities/namespaces.ts';
import { evaluateXPathToMap } from '../utilities/xquery.ts';
import type { BookmarkRangeEnd } from './BookmarkRangeEnd.ts';
Expand Down Expand Up @@ -80,6 +88,11 @@ export class Paragraph extends Component<ParagraphProps, ParagraphChild> {
public static override readonly mixed: boolean = false;
#sectionProperties: SectionProperties | null = null;

// For regular paragraphs this identifier is not required.
// It is when comments have replies. These "links" (X comment is a reply of Y comment)
// are handled via this identifier.
#id: Id | null = null;

/**
* Set properties to the section that this paragraph is supposed to represent. Not intended to be
* called manually. Only here because OOXML somehow decided that a section is defined in the last
Expand All @@ -89,21 +102,39 @@ export class Paragraph extends Component<ParagraphProps, ParagraphChild> {
this.#sectionProperties = properties || null;
}

/**
* Set the identifier (@w:paraId attribute) of this paragraph.
* This identifier is used by comment replies.
*/
public set id(id: Id) {
this.#id = id;
}

/**
* Creates an XML DOM node for this component instance.
*/
public override async toNode(ancestry: ComponentAncestor[]): Promise<Node> {
/**
* For some reason, MSWord requires the paraId attribute to have the w14 namespace, and at the

Choose a reason for hiding this comment

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

This scares me a bit... Could we find out something more in depth? It's the first thing I see in the project where ‘"w14" or ".w15" is used... wouldn't "w" be enough?

Choose a reason for hiding this comment

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

Clarification: It scares me because I have the feeling that it could lead to compatibility problems, but I can't prove my thoughts right now either.

Choose a reason for hiding this comment

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

Doing a quick test my Word uses those namespaces, I guess they are correct. Sorry for my monologue hahaha

Copy link
Author

Choose a reason for hiding this comment

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

I couldn't find a reason why this happens. MSWord things ¯_(ツ)_/¯.

* same time requires the w15 namespace in the commentsExtended.xml file for the same attribute
* 🤡
*/
return create(
`
element ${QNS.w}p {
if ($id) then attribute ${QNS.w14}paraId { $id } else (),
$pPr,
$children
}
`,
{
pPr: paragraphPropertiesToNode(this.props, this.#sectionProperties),
id: this.#id?.hex || null,
pPr: paragraphPropertiesToNode(
this.props,
this.#sectionProperties
),
children: await this.childrenToNode(ancestry),
},
}
);
}

Expand All @@ -118,13 +149,15 @@ export class Paragraph extends Component<ParagraphProps, ParagraphChild> {
* Instantiate this component from the XML in an existing DOCX file.
*/
static override fromNode(node: Node, context: ComponentContext): Paragraph {
const { children, ppr, ...props } = evaluateXPathToMap<{
const { children, ppr, id, ...props } = evaluateXPathToMap<{
ppr: Node;
children: Node[];
id?: string;
style?: string;
}>(
`
map {
"id": @${QNS.w14}paraId/string(),
"ppr": ./${QNS.w}pPr,
"style": ./${QNS.w}pPr/${QNS.w}pStyle/@${QNS.w}val/string(),
"children": array{ ./(
Expand All @@ -140,16 +173,26 @@ export class Paragraph extends Component<ParagraphProps, ParagraphChild> {
) }
}
`,
node,
node
);

return new Paragraph(
const paragraph = new Paragraph(
{
...paragraphPropertiesFromNode(ppr),
...props,
},
...createChildComponentsFromNodes<ParagraphChild>(this.children, children, context),
...createChildComponentsFromNodes<ParagraphChild>(
this.children,
children,
context
)
);

if (id) {
paragraph.id = hex(id);
}

return paragraph;
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum FileLocation {
// sensible/conventional defaults:

comments = 'word/comments.xml',
commentsExtended = 'word/commentsExtended.xml',
coreProperties = 'docProps/core.xml',
mainDocument = 'word/document.xml',
numbering = 'word/numbering.xml',
Expand Down Expand Up @@ -36,6 +37,7 @@ export enum FileMime {
theme = 'application/vnd.openxmlformats-officedocument.theme+xml',
webSettings = 'application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml',
comments = 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml',
commentsExtended = 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml',
numbering = 'application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml',

// Images
Expand Down Expand Up @@ -73,6 +75,6 @@ export enum RelationshipType {
// For dotx created with office 2020
classificationlabels = 'http://schemas.microsoft.com/office/2020/02/relationships/classificationlabels',
// Legacy template (.dot)
downRev = "http://schemas.microsoft.com/office/2006/relationships/downRev",
graphicFrameDoc = "http://schemas.microsoft.com/office/2006/relationships/graphicFrameDoc",
downRev = 'http://schemas.microsoft.com/office/2006/relationships/downRev',
graphicFrameDoc = 'http://schemas.microsoft.com/office/2006/relationships/graphicFrameDoc',
}
Loading
0