Skip to content

Commit 28c3a02

Browse files
feat: parse rich smart chips (person, date, rich link) in Google Docs (#263)
* Enable parsing of rich (smart) tags in google docs Currently docs.getText skip over smart chip elements within Google docs. Leaving out information. This commit adds support to render smart chip elements in google docs to simple text * Update the text output * feat: parse rich smart chips (person, date, rich link) in Google Docs Adds smart chip parsing to the DocsService getText method, rendering: - Person chips as markdown mailto links with name fallback to email - Rich link chips as markdown links with title fallback to URI - Date chips as displayText with fallback to timestamp Based on PR #215 by @MrwanBaghdad. Fixes reviewed items: - Rich link title fallback bug (title || uri) - Missing rich link fallback test - Refactored fallback tests to it.each - Prettier formatting applied Co-authored-by: MrwanBaghdad <marwan.nabil@deliveryhero.com> * fix: add defensive guards for missing email and uri in smart chips - Person chips: guard against missing email, fall back to name only - Rich link chips: guard against missing uri, fall back to title only - Added 2 new it.each test cases for these edge cases Addresses review feedback from Gemini Code Assist on PR #263 * refactor: improve smart chip readability with helper methods - Extract _renderPersonChip, _renderRichLinkChip, _renderDateChip from _readStructuralElement for cleaner dispatch logic - Add mockDocWithElements test helper to reduce mock boilerplate Addresses readability nits from @abhipatel12 on PR #263 --------- Co-authored-by: MrwanBaghdad <marwan.nabil@deliveryhero.com>
1 parent 9318534 commit 28c3a02

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

workspace-server/src/__tests__/services/DocsService.test.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,133 @@ describe('DocsService', () => {
736736
});
737737
});
738738

739+
/** Helper to wrap paragraph elements in the standard mock doc structure. */
740+
const mockDocWithElements = (...elements: Record<string, unknown>[]) => ({
741+
data: {
742+
tabs: [
743+
{
744+
documentTab: {
745+
body: {
746+
content: [{ paragraph: { elements } }],
747+
},
748+
},
749+
},
750+
],
751+
},
752+
});
753+
754+
it('should extract text from smart chips (date, person, rich link)', async () => {
755+
const mockDoc = mockDocWithElements(
756+
{ textRun: { content: 'Meeting on ' } },
757+
{
758+
dateElement: {
759+
dateElementProperties: {
760+
displayText: 'Jan 15, 2025',
761+
timestamp: '1736899200',
762+
},
763+
},
764+
},
765+
{ textRun: { content: ' with ' } },
766+
{
767+
person: {
768+
personProperties: {
769+
name: 'John Doe',
770+
email: 'john@example.com',
771+
},
772+
},
773+
},
774+
{ textRun: { content: ' - see ' } },
775+
{
776+
richLink: {
777+
richLinkProperties: {
778+
title: 'Project Plan',
779+
uri: 'https://docs.google.com/document/d/abc123',
780+
},
781+
},
782+
},
783+
{ textRun: { content: '\n' } },
784+
);
785+
mockDocsAPI.documents.get.mockResolvedValue(mockDoc);
786+
787+
const result = await docsService.getText({ documentId: 'test-doc-id' });
788+
789+
expect(result.content[0].text).toBe(
790+
'Meeting on Jan 15, 2025 with [John Doe](mailto:john@example.com) - see [Project Plan](https://docs.google.com/document/d/abc123)\n',
791+
);
792+
});
793+
794+
it.each([
795+
{
796+
name: 'person without name falls back to email',
797+
element: {
798+
person: {
799+
personProperties: {
800+
email: 'jane@example.com',
801+
},
802+
},
803+
},
804+
expected: '[jane@example.com](mailto:jane@example.com)',
805+
},
806+
{
807+
name: 'person without email falls back to name only',
808+
element: {
809+
person: {
810+
personProperties: {
811+
name: 'John Doe',
812+
},
813+
},
814+
},
815+
expected: 'John Doe',
816+
},
817+
{
818+
name: 'rich link without title falls back to uri',
819+
element: {
820+
richLink: {
821+
richLinkProperties: {
822+
uri: 'https://docs.google.com/spreadsheets/d/xyz',
823+
},
824+
},
825+
},
826+
expected:
827+
'[https://docs.google.com/spreadsheets/d/xyz](https://docs.google.com/spreadsheets/d/xyz)',
828+
},
829+
{
830+
name: 'rich link without uri falls back to title only',
831+
element: {
832+
richLink: {
833+
richLinkProperties: {
834+
title: 'Some Document',
835+
},
836+
},
837+
},
838+
expected: 'Some Document',
839+
},
840+
{
841+
name: 'date without displayText falls back to timestamp',
842+
element: {
843+
dateElement: {
844+
dateElementProperties: {
845+
timestamp: '1736899200',
846+
},
847+
},
848+
},
849+
expected: '1736899200',
850+
},
851+
])(
852+
'should fall back correctly when $name',
853+
async ({ element, expected }) => {
854+
mockDocsAPI.documents.get.mockResolvedValue(
855+
mockDocWithElements(element),
856+
);
857+
858+
const result = await docsService.getText({
859+
documentId: 'test-doc-id',
860+
});
861+
862+
expect(result.content[0].text).toBe(expected);
863+
},
864+
);
865+
739866
it('should include text from nested child tabs', async () => {
740867
const mockDoc = {
741868
data: {

workspace-server/src/services/DocsService.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,16 @@ export class DocsService {
816816
element.paragraph.elements?.forEach((pElement) => {
817817
if (pElement.textRun && pElement.textRun.content) {
818818
text += pElement.textRun.content;
819+
} else if (pElement.person?.personProperties) {
820+
text += this._renderPersonChip(pElement.person.personProperties);
821+
} else if (pElement.richLink?.richLinkProperties) {
822+
text += this._renderRichLinkChip(
823+
pElement.richLink.richLinkProperties,
824+
);
825+
} else if (pElement.dateElement?.dateElementProperties) {
826+
text += this._renderDateChip(
827+
pElement.dateElement.dateElementProperties,
828+
);
819829
}
820830
});
821831
} else if (element.table) {
@@ -830,6 +840,29 @@ export class DocsService {
830840
return text;
831841
}
832842

843+
private _renderPersonChip(props: docs_v1.Schema$PersonProperties): string {
844+
const { name, email } = props;
845+
if (email) {
846+
return `[${name || email}](mailto:${email})`;
847+
}
848+
return name || '';
849+
}
850+
851+
private _renderRichLinkChip(
852+
props: docs_v1.Schema$RichLinkProperties,
853+
): string {
854+
const { title, uri } = props;
855+
if (uri) {
856+
return `[${title || uri}](${uri})`;
857+
}
858+
return title || '';
859+
}
860+
861+
private _renderDateChip(props: docs_v1.Schema$DateElementProperties): string {
862+
const { displayText, timestamp } = props;
863+
return displayText || timestamp || '';
864+
}
865+
833866
public replaceText = async ({
834867
documentId,
835868
findText,

0 commit comments

Comments
 (0)