Skip to content

Commit c7b45ac

Browse files
authored
Merge pull request #23 from RefactorSecurity/export-notes
Export notes
2 parents 9ead0be + 30f5fc1 commit c7b45ac

11 files changed

Lines changed: 578 additions & 420 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ gosec -fmt=json -out=gosec-results.json ./...
9191
semgrep scan --json -o semgrep-results.json --config=auto .
9292
```
9393

94+
## Exporting notes in popular formats
95+
96+
Currently we only support exporting notes to Markdown, but other formats such as HTML are coming soon.
97+
9498
## Extension Settings
9599

96100
Various settings for the extension can be configured in VSCode's User Settings page (`CMD+Shift+P` / `Ctrl + Shift + P` -> _Preferences: Open Settings (UI)_):

package-lock.json

Lines changed: 261 additions & 391 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Security Notes",
44
"description": "Create notes during a security code review. Import your favorite SAST tool results and collaborate with others.",
55
"icon": "resources/security_notes_logo.png",
6-
"version": "1.2.0",
6+
"version": "1.3.0",
77
"publisher": "refactor-security",
88
"private": false,
99
"license": "MIT",
@@ -233,6 +233,11 @@
233233
"type": "webview",
234234
"name": "Import Tool Results",
235235
"id": "import-tool-results-view"
236+
},
237+
{
238+
"type": "webview",
239+
"name": "Export Notes",
240+
"id": "export-notes-view"
236241
}
237242
]
238243
},

resources/close_inverse.svg

Lines changed: 4 additions & 2 deletions
Loading

src/extension.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import * as vscode from 'vscode';
44
import { NoteStatus } from './models/noteStatus';
55
import { NoteComment } from './models/noteComment';
66
import { Resource } from './reactions/resource';
7-
import { ImportToolResultsWebview } from './webviews/importToolResultsWebview';
7+
import { ImportToolResultsWebview } from './webviews/import-tool-results/importToolResultsWebview';
8+
import { ExportNotesWebview } from './webviews/export-notes/exportNotesWebview';
89
import { commentController } from './controllers/comments';
910
import { reactionHandler } from './handlers/reaction';
1011
import { saveNotesToFileHandler } from './handlers/saveNotesToFile';
@@ -232,6 +233,15 @@ export function activate(context: vscode.ExtensionContext) {
232233
),
233234
);
234235

236+
// webview for exporting notes
237+
const exportNotesWebview = new ExportNotesWebview(context.extensionUri, noteMap);
238+
context.subscriptions.push(
239+
vscode.window.registerWebviewViewProvider(
240+
ExportNotesWebview.viewType,
241+
exportNotesWebview,
242+
),
243+
);
244+
235245
// load persisted comments from file
236246
const persistedThreads = loadNotesFromFile();
237247
persistedThreads.forEach((thread) => {

src/webviews/assets/exportNotes.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint-disable no-undef */
2+
3+
// This script will be run within the webview itself
4+
// It cannot access the main VS Code APIs directly.
5+
(function () {
6+
const vscode = acquireVsCodeApi();
7+
8+
document
9+
.querySelector('.export-notes-button')
10+
.addEventListener('click', () => onButtonClicked());
11+
12+
function onButtonClicked() {
13+
// selected notes
14+
let vulnerable = document.getElementById('vulnerable-notes').checked;
15+
let notVulnerable = document.getElementById('not-vulnerable-notes').checked;
16+
let todo = document.getElementById('todo-notes').checked;
17+
let noStatus = document.getElementById('no-status-notes').checked;
18+
let status = {
19+
vulnerable,
20+
notVulnerable,
21+
todo,
22+
noStatus,
23+
};
24+
25+
// additional options
26+
let includeCodeSnippet = document.getElementById('include-code-snippet').checked;
27+
let includeReplies = document.getElementById('include-note-replies').checked;
28+
let includeAuthors = document.getElementById('include-authors').checked;
29+
let options = {
30+
includeCodeSnippet,
31+
includeReplies,
32+
includeAuthors,
33+
};
34+
35+
// export format
36+
let formatSelect = document.getElementById('format-select');
37+
let format = formatSelect.options[formatSelect.selectedIndex].value;
38+
39+
vscode.postMessage({ type: 'exportNotes', status, options, format });
40+
}
41+
})();

src/webviews/assets/reset.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ h3,
1616
h4,
1717
h5,
1818
h6,
19-
p,
2019
ol,
2120
ul {
2221
margin: 0;
@@ -28,3 +27,8 @@ img {
2827
max-width: 100%;
2928
height: auto;
3029
}
30+
31+
p {
32+
line-height: 25px; /* within paragraph */
33+
font-weight: normal;
34+
}

src/webviews/assets/vscode.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ button.secondary:hover {
7373
background: var(--vscode-button-secondaryHoverBackground);
7474
}
7575

76-
input:not([type='checkbox']),
76+
input:not([type='checkbox']):not([type='radio']),
7777
select,
7878
textarea {
7979
display: block;
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
'use strict';
2+
3+
import * as vscode from 'vscode';
4+
import { fullPathToRelative } from '../../utils';
5+
6+
export class ExportNotesWebview implements vscode.WebviewViewProvider {
7+
public static readonly viewType = 'export-notes-view';
8+
9+
private _view?: vscode.WebviewView;
10+
private noteMap: Map<string, vscode.CommentThread>;
11+
12+
constructor(
13+
private readonly _extensionUri: vscode.Uri,
14+
noteMap: Map<string, vscode.CommentThread>,
15+
) {
16+
this.noteMap = noteMap;
17+
}
18+
19+
public resolveWebviewView(
20+
webviewView: vscode.WebviewView,
21+
_context: vscode.WebviewViewResolveContext,
22+
_token: vscode.CancellationToken,
23+
) {
24+
this._view = webviewView;
25+
26+
webviewView.webview.options = {
27+
// Allow scripts in the webview
28+
enableScripts: true,
29+
localResourceRoots: [this._extensionUri],
30+
};
31+
32+
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
33+
34+
webviewView.webview.onDidReceiveMessage((data) => {
35+
switch (data.type) {
36+
case 'exportNotes': {
37+
exportNotes(data.status, data.options, data.format, this.noteMap);
38+
}
39+
}
40+
});
41+
}
42+
43+
private _getHtmlForWebview(webview: vscode.Webview) {
44+
const scriptUri = webview.asWebviewUri(
45+
vscode.Uri.joinPath(
46+
this._extensionUri,
47+
'src',
48+
'webviews',
49+
'assets',
50+
'exportNotes.js',
51+
),
52+
);
53+
const styleResetUri = webview.asWebviewUri(
54+
vscode.Uri.joinPath(this._extensionUri, 'src', 'webviews', 'assets', 'reset.css'),
55+
);
56+
const styleVSCodeUri = webview.asWebviewUri(
57+
vscode.Uri.joinPath(
58+
this._extensionUri,
59+
'src',
60+
'webviews',
61+
'assets',
62+
'vscode.css',
63+
),
64+
);
65+
const styleMainUri = webview.asWebviewUri(
66+
vscode.Uri.joinPath(this._extensionUri, 'src', 'webviews', 'assets', 'main.css'),
67+
);
68+
69+
return `<!DOCTYPE html>
70+
<html lang="en">
71+
<head>
72+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
73+
<link href="${styleResetUri}" rel="stylesheet">
74+
<link href="${styleVSCodeUri}" rel="stylesheet">
75+
<link href="${styleMainUri}" rel="stylesheet">
76+
</head>
77+
<body>
78+
<p>Select the notes you want to export:</p>
79+
<p>
80+
<input type="checkbox" id="vulnerable-notes" value="vulnerable-notes" checked>
81+
<label for="vulnerable-notes"> Vulnerable</label></br>
82+
<input type="checkbox" id="not-vulnerable-notes" value="not-vulnerable-notes" checked>
83+
<label for="not-vulnerable-notes"> Not Vulnerable</label></br>
84+
<input type="checkbox" id="todo-notes" value="todo-notes" checked>
85+
<label for="todo-notes"> TODO</label></br>
86+
<input type="checkbox" id="no-status-notes" value="no-status-notes" checked>
87+
<label for="no-status-notes"> No Status</label></br>
88+
</p>
89+
</br>
90+
91+
<p>Select additional options:</p>
92+
<p>
93+
<input type="checkbox" id="include-code-snippet" name="include-code-snippet" value="include-code-snippet" checked>
94+
<label for="include-code-snippet"> Include code snippet</label></br>
95+
</p>
96+
<p>
97+
<input type="checkbox" id="include-note-replies" name="include-note-replies" value="include-note-replies" checked>
98+
<label for="include-note-replies"> Include note replies</label></br>
99+
</p>
100+
<p>
101+
<input type="checkbox" id="include-authors" name="include-authors" value="include-authors" checked>
102+
<label for="include-authors"> Include authors</label></br>
103+
</p>
104+
</br>
105+
106+
<p>Select export format:</p>
107+
<p>
108+
<select id="format-select">
109+
<option value="markdown">Markdown</option>
110+
</select>
111+
</p>
112+
</br>
113+
114+
<p>
115+
<button class="export-notes-button">Export</button>
116+
</p>
117+
118+
<script src="${scriptUri}"></script>
119+
</body>
120+
</html>`;
121+
}
122+
}
123+
124+
async function exportNotes(
125+
status: any,
126+
options: any,
127+
format: string,
128+
noteMap: Map<string, vscode.CommentThread>,
129+
) {
130+
// filter notes based on selected status
131+
const selectedNotes = [...noteMap]
132+
.map(([_id, note]) => {
133+
const firstComment = note.comments[0].body.toString();
134+
if (
135+
(status.vulnerable && firstComment.startsWith('[Vulnerable] ')) ||
136+
(status.notVulnerable && firstComment.startsWith('[Not Vulnerable] ')) ||
137+
(status.todo && firstComment.startsWith('[TODO] ')) ||
138+
status.noStatus
139+
) {
140+
return note;
141+
}
142+
})
143+
.filter((element) => element !== undefined);
144+
145+
if (!selectedNotes.length) {
146+
vscode.window.showInformationMessage('[Export] No notes met the criteria.');
147+
return;
148+
}
149+
150+
switch (format) {
151+
case 'markdown': {
152+
const outputs = await Promise.all(
153+
selectedNotes.map(async (note: any) => {
154+
// include code snippet
155+
if (options.includeCodeSnippet) {
156+
const codeSnippet = await exportCodeSnippet(note.uri, note.range);
157+
return codeSnippet + exportComments(note, options);
158+
}
159+
return exportComments(note, options);
160+
}),
161+
);
162+
163+
const document = await vscode.workspace.openTextDocument({
164+
content: outputs.join(''),
165+
language: 'markdown',
166+
});
167+
vscode.window.showTextDocument(document);
168+
}
169+
}
170+
}
171+
172+
function exportComments(note: vscode.CommentThread, options: any) {
173+
// export first comment
174+
let output = '';
175+
output += exportComment(
176+
note.comments[0].body.toString(),
177+
options.includeAuthors ? note.comments[0].author.name : undefined,
178+
);
179+
180+
// include replies
181+
if (options.includeReplies) {
182+
note.comments.slice(1).forEach((comment: vscode.Comment) => {
183+
output += exportComment(
184+
comment.body.toString(),
185+
options.includeAuthors ? comment.author.name : undefined,
186+
);
187+
});
188+
}
189+
190+
output += `\n-----\n`;
191+
return output;
192+
}
193+
194+
function exportComment(body: string, author: string | undefined) {
195+
if (author) {
196+
return `\n**${author}** - ${body}\n`;
197+
}
198+
return `\n${body}\n`;
199+
}
200+
201+
async function exportCodeSnippet(uri: vscode.Uri, range: vscode.Range) {
202+
const output = await vscode.workspace.openTextDocument(uri).then(async (document) => {
203+
const newRange = new vscode.Range(range.start.line, 0, range.end.line + 1, 0);
204+
const codeSnippet = await document.getText(newRange).trimEnd();
205+
return `\nCode snippet \`${fullPathToRelative(
206+
uri.fsPath,
207+
)}\`:\n\n\`\`\`\n${codeSnippet}\n\`\`\`\n`;
208+
});
209+
return output;
210+
}

0 commit comments

Comments
 (0)