Skip to content

Commit

Permalink
Add option to export annotations in markdown
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Jan 25, 2024
1 parent e4427ce commit 3257bf7
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 7 deletions.
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 @@ export class AnnotationsExporter {
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 @@ export class AnnotationsExporter {
).replace(/\t/g, ' ');
}

buildMarkdownExportContent(
annotations: APIAnnotationData[],
{
groupName = '',
displayNamesEnabled = false,
defaultAuthority = '',
/* istanbul ignore next - test seam */
now = new Date(),
}: HTMLExportOptions = {},
): string {
const { uri, title, uniqueUsers, replies, extractUsername } =
this._exportCommon(annotations, {
displayNamesEnabled,
defaultAuthority,
});

const quoteToMarkdown = (quote: string): string => trimAndDedent`
* Quote:
${quote
.split('\n')
.map(quoteLine => ` > ${quoteLine.trim()}`)
.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`
* Comment:
\`\`\`
${text
.split('\n')
.map(textLine => ` ${textLine.trim()}`)
.join('\n')}
\`\`\`
`;

return trimAndDedent`
# 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);
const lines = [
`* Created at: ${formatDateTime(new Date(annotation.created))}`,
`* Author: ${extractUsername(annotation)}`,
page && `* Page: ${page}`,
`* Type: ${annotationRole(annotation)}`,
annotationQuote && quoteToMarkdown(annotationQuote),
textToMarkdown(annotation.text),
annotation.tags.length > 0 &&
`* Tags: ${annotation.tags.join(', ')}`,
].filter(Boolean);
return trimAndDedent`
## Annotation ${index + 1}:
${lines.join('\n')}`;
})
.join('\n\n')}
`;
}

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

0 comments on commit 3257bf7

Please sign in to comment.