Skip to content
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

Add option to export annotations in markdown #6141

Closed
wants to merge 1 commit into from
Closed
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
18 changes: 17 additions & 1 deletion src/sidebar/components/ShareDialog/ExportAnnotations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type ExportAnnotationsProps = {

type ExportFormat = {
/** Unique format identifier used also as file extension */
value: 'json' | 'csv' | 'txt' | 'html';
value: 'json' | 'csv' | 'txt' | 'html' | 'md';
/** The title to be displayed in the listbox item */
title: string;

Expand Down Expand Up @@ -67,6 +67,12 @@ const exportFormats: ExportFormat[] = [
shortTitle: 'HTML',
description: 'For import into word processors as rich text',
},
{
value: 'md',
title: 'Markdown (MD)',
shortTitle: 'MD',
description: 'For import into markdown based editors',
},
];

function formatToMimeType(format: ExportFormat['value']): string {
Expand All @@ -75,6 +81,7 @@ function formatToMimeType(format: ExportFormat['value']): string {
txt: 'text/plain',
csv: 'text/csv',
html: 'text/html',
md: 'text/markdown',
};
return typeForFormat[format];
}
Expand Down Expand Up @@ -193,6 +200,15 @@ function ExportAnnotations({
},
);
}
case 'md':
return annotationsExporter.buildMarkdownExportContent(
annotationsToExport,
{
groupName: group?.name,
defaultAuthority,
displayNamesEnabled,
},
);
/* istanbul ignore next - This should never happen */
default:
throw new Error(`Invalid format: ${format}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('ExportAnnotations', () => {
buildTextExportContent: sinon.stub().returns(''),
buildCSVExportContent: sinon.stub().returns(''),
buildHTMLExportContent: sinon.stub().returns(''),
buildMarkdownExportContent: sinon.stub().returns(''),
};
fakeToastMessenger = {
error: sinon.stub(),
Expand Down Expand Up @@ -258,7 +259,7 @@ describe('ExportAnnotations', () => {
const optionText = (index, type) =>
options.at(index).find(`[data-testid="format-${type}"]`).text();

assert.equal(options.length, 4);
assert.equal(options.length, 5);
assert.equal(optionText(0, 'name'), 'JSON');
assert.equal(
optionText(0, 'description'),
Expand All @@ -276,6 +277,11 @@ describe('ExportAnnotations', () => {
optionText(3, 'description'),
'For import into word processors as rich text',
);
assert.equal(optionText(4, 'name'), 'Markdown (MD)');
assert.equal(
optionText(4, 'description'),
'For import into markdown based editors',
);
});

[
Expand Down Expand Up @@ -325,6 +331,12 @@ describe('ExportAnnotations', () => {
fakeAnnotationsExporter.buildHTMLExportContent,
],
},
{
format: 'md',
getExpectedInvokedContentBuilder: () => [
fakeAnnotationsExporter.buildMarkdownExportContent,
],
},
].forEach(({ format, getExpectedInvokedContentBuilder }) => {
it('builds an export file from all non-draft annotations', async () => {
const wrapper = createComponent();
Expand Down Expand Up @@ -418,6 +430,10 @@ describe('ExportAnnotations', () => {
format: 'html',
expectedMimeType: 'text/html',
},
{
format: 'md',
expectedMimeType: 'text/markdown',
},
].forEach(({ format, expectedMimeType }) => {
it('downloads a file using user-entered filename appended with proper extension', async () => {
const wrapper = createComponent();
Expand Down Expand Up @@ -508,6 +524,13 @@ describe('ExportAnnotations', () => {
fakeAnnotationsExporter.buildHTMLExportContent,
],
},
{
format: 'md',
getExpectedInvokedCallback: () => fakeCopyPlainText,
getExpectedInvokedContentBuilder: () => [
fakeAnnotationsExporter.buildMarkdownExportContent,
],
},
].forEach(
({
format,
Expand Down
85 changes: 80 additions & 5 deletions src/sidebar/services/annotations-exporter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,11 @@
const lines = [
`Created at: ${formatDateTime(new Date(annotation.created))}`,
`Author: ${extractUsername(annotation)}`,
page ? `Page: ${page}` : undefined,
page && `Page: ${page}`,
`Type: ${annotationRole(annotation)}`,
annotationQuote ? `Quote: "${annotationQuote}"` : undefined,
annotationQuote && `Quote: "${annotationQuote}"`,
`Comment: ${annotation.text}`,
annotation.tags.length > 0
? `Tags: ${annotation.tags.join(', ')}`
: undefined,
annotation.tags.length > 0 && `Tags: ${annotation.tags.join(', ')}`,
].filter(Boolean);

return trimAndDedent`
Expand Down Expand Up @@ -311,6 +309,83 @@
).replace(/\t/g, ' ');
}

buildMarkdownExportContent(
annotations: APIAnnotationData[],
{
groupName = '',
displayNamesEnabled = false,
defaultAuthority = '',

Check warning on line 317 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L315-L317

Added lines #L315 - L317 were not covered by tests
/* istanbul ignore next - test seam */
now = new Date(),
}: HTMLExportOptions = {},
): string {

Check warning on line 321 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L319-L321

Added lines #L319 - L321 were not covered by tests
const { uri, title, uniqueUsers, replies, extractUsername } =
this._exportCommon(annotations, {

Check warning on line 323 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L323

Added line #L323 was not covered by tests
displayNamesEnabled,
defaultAuthority,
});

const quoteToMarkdown = (quote: string): string => trimAndDedent`

Check warning on line 328 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L328

Added line #L328 was not covered by tests
* Quote:
${quote
.split('\n')
.map(quoteLine => ` > ${quoteLine.trim()}`)

Check warning on line 332 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L332

Added line #L332 was not covered by tests
.join('\n')}
`;
// Since annotations text is already markdown, we want to wrap it in a pre
// to avoid it to be rendered by markdown parsers
const textToMarkdown = (text: string): string => trimAndDedent`

Check warning on line 337 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L337

Added line #L337 was not covered by tests
* Comment:
\`\`\`
${text
.split('\n')
.map(textLine => ` ${textLine.trim()}`)

Check warning on line 342 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L342

Added line #L342 was not covered by tests
.join('\n')}
\`\`\`
`;

return trimAndDedent`

Check warning on line 347 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L347

Added line #L347 was not covered by tests
# Annotations on "${title}"

${formatDateTime(now)}

[${uri}](${uri})

* Group: ${groupName}
* Total users: ${uniqueUsers.length}
* Users: ${uniqueUsers.join(', ')}
* Total annotations: ${annotations.length}
* Total replies: ${replies.length}

* * *

# Annotations

${annotations
.map((annotation, index) => {
const page = pageLabel(annotation);
const annotationQuote = quote(annotation);

Check warning on line 367 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L365-L367

Added lines #L365 - L367 were not covered by tests

const lines = [

Check warning on line 369 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L369

Added line #L369 was not covered by tests
`* Created at: ${formatDateTime(new Date(annotation.created))}`,
`* Author: ${extractUsername(annotation)}`,
page && `* Page: ${page}`,

Check warning on line 372 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L372

Added line #L372 was not covered by tests
`* Type: ${annotationRole(annotation)}`,
annotationQuote && quoteToMarkdown(annotationQuote),

Check warning on line 374 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L374

Added line #L374 was not covered by tests
textToMarkdown(annotation.text),
annotation.tags.length > 0 &&
`* Tags: ${annotation.tags.join(', ')}`,

Check warning on line 377 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L376-L377

Added lines #L376 - L377 were not covered by tests
].filter(Boolean);

return trimAndDedent`

Check warning on line 380 in src/sidebar/services/annotations-exporter.tsx

View check run for this annotation

Codecov / codecov/patch

src/sidebar/services/annotations-exporter.tsx#L380

Added line #L380 was not covered by tests
## Annotation ${index + 1}:

${lines.join('\n')}`;
})
.join('\n\n')}
`;
}

private _exportCommon(
annotations: APIAnnotationData[],
{
Expand Down
Loading