diff --git a/build.gradle b/build.gradle
index b5827976aa..70188931d8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -38,6 +38,7 @@ dependencies {
implementation group: 'org.apache.ant', name: 'ant', version: '1.10.12'
implementation group: 'org.apache.commons', name: 'commons-csv', version: '1.9.0'
implementation group: 'org.fusesource.jansi', name: 'jansi', version: '2.4.0'
+ implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.17.0'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion
diff --git a/config/blurbs.md b/config/blurbs.md
new file mode 100644
index 0000000000..6ccea2c23e
--- /dev/null
+++ b/config/blurbs.md
@@ -0,0 +1,2 @@
+https://github.com/reposense/testrepo-Alpha/tree/master
+Master branch of testrepo-Alpha
diff --git a/docs/ug/blurbs.md b/docs/ug/blurbs.md
new file mode 100644
index 0000000000..4219a83fb3
--- /dev/null
+++ b/docs/ug/blurbs.md
@@ -0,0 +1,5 @@
+https://github.com/reposense/RepoSense/tree/cypress
+Cypress branch of RepoSense
+------------------------------------
+https://github.com/reposense/publish-RepoSense/tree/master
+Publishing branch of RepoSense
diff --git a/docs/ug/cli.md b/docs/ug/cli.md
index 550713969c..02f5cab297 100644
--- a/docs/ug/cli.md
+++ b/docs/ug/cli.md
@@ -68,7 +68,7 @@ partial credit.
**`--config CONFIG_DIRECTORY`**: Specifies that config files located in `CONFIG_DIRECTORY` should be used to customize the report.
-* Parameter: `CONFIG_DIRECTORY` The directory containing the config files. Should contain a `repo-config.csv` file. Optionally, can contain an `author-config.csv` file or/and a `group-config.csv` file or/and a `report-config.json` file.
+* Parameter: `CONFIG_DIRECTORY` The directory containing the config files. Should contain a `repo-config.csv` file. Optionally, can contain an `author-config.csv` file or/and a `group-config.csv` file or/and a `report-config.json` file or/and a `blurbs.md` file.
* Alias: `-c`
* Example: `java -jar RepoSense.jar --config ./config`
diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md
index 1797f8e4ea..5a9c1745c2 100644
--- a/docs/ug/configFiles.md
+++ b/docs/ug/configFiles.md
@@ -209,3 +209,17 @@ Note: Symbols such as `"`, `!`, `/` etc. in your author name will be omitted, wh
+
+
+
+
+
+## `blurbs.md`
+
+You can optionally use `blurbs.md` to add blurbs in Markdown syntax for repository branches. These blurbs will be seen when grouping by `Repo/Branch`. ([example](https://github.com/reposense/RepoSense/blob/master/docs/ug/blurbs.md))
+
+**Format**:
+* First line in section: Link to the repository branch.
+* Second line onwards: Blurb content.
+* Delimiter: ``. Everything on the line after the delimiter will be ignored.
+
diff --git a/docs/ug/customizingReports.md b/docs/ug/customizingReports.md
index da6ad7f49e..531ae94442 100644
--- a/docs/ug/customizingReports.md
+++ b/docs/ug/customizingReports.md
@@ -62,8 +62,9 @@ In both instances, it is **necessary to commit any changes** for them to be dete
-### Add a title
+### Personalizing Reports
+#### Add a title
A title component can be added by creating a file titled `title.md` in the assets directory. You can specify the assets directory according to the reference below:
{{ embed("Appendix: **CLI syntax reference → `assets` flag**", "cli.md#section-assets") }}
@@ -73,3 +74,10 @@ The title can render a combination of Markdown/HTML and plaintext ([example](htt
Do note that the width of the title is bound by the width of the left panel.
For more information on how to use Markdown, see the [Markdown Guide](https://www.markdownguide.org/).
+
+#### Add blurbs for branches
+A blurb can be added for a repository branch by creating a file titled `blurbs.md` in the config directory. The blurbs will be visible when grouping by `Repo/Branch`. The format of the file is given below:
+{{ embed("Appendix: **Config files format**", "configFiles.md#section-blurbs") }}
+
+Specifying the config directory can be done as follows:
+{{ embed("Appendix: **CLI syntax reference → `config` flag**", "cli.md#section-config") }}
diff --git a/frontend/cypress/config/blurbs.md b/frontend/cypress/config/blurbs.md
new file mode 100644
index 0000000000..aec0130427
--- /dev/null
+++ b/frontend/cypress/config/blurbs.md
@@ -0,0 +1,11 @@
+https://github.com/reposense/RepoSense/tree/cypress
+first blurb
+------------------------------------
+https://gitlab.com/reposense/testrepo-gitlab/-/tree/main
+unseen blurb
+------------------------------------
+https://github.com/reposense/publish-RepoSense/tree/master
+## third blurb in h2 markdown tag
+------------------------------------
+https://github.com/reposense/RepoSense-auth-helper/tree/master
+second blurb in h1 tag
diff --git a/frontend/cypress/tests/chartView/chartView_blurbs.cy.js b/frontend/cypress/tests/chartView/chartView_blurbs.cy.js
new file mode 100644
index 0000000000..71c5d62902
--- /dev/null
+++ b/frontend/cypress/tests/chartView/chartView_blurbs.cy.js
@@ -0,0 +1,34 @@
+describe('blurbs', () => {
+ it('shows blurbs', () => {
+ cy.get('.markdown.blurb')
+ .first()
+ .should('contain', 'first blurb');
+
+ cy.get('.markdown.blurb')
+ .eq(1)
+ .should('contain', 'second blurb');
+
+ cy.get('.markdown.blurb')
+ .eq(2)
+ .should('contain', 'third blurb');
+ });
+
+ it('has the correct number of valid blurbs', () => {
+ cy.get('.markdown.blurb')
+ .should('have.length', 3);
+ });
+
+ it('processes markdown in blurbs', () => {
+ cy.get('.markdown.blurb')
+ .eq(1)
+ .find('h1')
+ .contains('second blurb in h1 tag');
+ });
+
+ it('processes html in blurbs', () => {
+ cy.get('.markdown.blurb')
+ .eq(2)
+ .find('h2')
+ .contains('third blurb in h2 markdown tag');
+ });
+});
diff --git a/frontend/src/app.vue b/frontend/src/app.vue
index fa6b442f4a..0c2959d0a6 100644
--- a/frontend/src/app.vue
+++ b/frontend/src/app.vue
@@ -122,6 +122,7 @@ const app = defineComponent({
reportGenerationTime,
errorMessages,
names,
+ blurbMap,
} = summary;
this.creationDate = creationDate;
this.reportGenerationTime = reportGenerationTime;
@@ -134,6 +135,7 @@ const app = defineComponent({
this.getUsers();
this.renderTabHash();
this.userUpdated = true;
+ this.$store.commit('setBlurbMap', blurbMap);
} catch (error) {
window.alert(error);
} finally {
diff --git a/frontend/src/components/c-markdown-chunk.vue b/frontend/src/components/c-markdown-chunk.vue
new file mode 100644
index 0000000000..47dccb392d
--- /dev/null
+++ b/frontend/src/components/c-markdown-chunk.vue
@@ -0,0 +1,30 @@
+
+.markdown(v-html="parsedHtml", v-if="markdownText != ''")
+
+
+
+
+
diff --git a/frontend/src/components/c-summary-charts.vue b/frontend/src/components/c-summary-charts.vue
index 437b23e5ea..bb0822eeb6 100644
--- a/frontend/src/components/c-summary-charts.vue
+++ b/frontend/src/components/c-summary-charts.vue
@@ -141,9 +141,8 @@
)
font-awesome-icon.icon-button(icon="clipboard")
span.tooltip-text(v-bind:ref="`summary-charts-${i}-copy-iframe`") Click to copy iframe link for group
-
.tooltip.summary-chart__title--percentile(
- v-if="sortGroupSelection.includes('totalCommits')"
+ v-if="sortGroupSelection.includes('totalCommits')"
) {{ getPercentile(i) }} % 
span.tooltip-text.right-aligned {{ getPercentileExplanation(i) }}
.summary-charts__title--tags(
@@ -158,6 +157,14 @@
)
font-awesome-icon(icon="tags")
span {{ tag }}
+
+ .blurbWrapper(
+ v-if="filterGroupSelection === 'groupByRepos'",
+ )
+ c-markdown-chunk.blurb(
+ v-bind:markdown-text="getBlurb(repo[0])"
+ )
+
.summary-charts__fileType--breakdown(v-if="filterBreakdown")
template(v-if="filterGroupSelection !== 'groupByNone'")
.summary-charts__fileType--breakdown__legend(
@@ -337,6 +344,7 @@ import brokenLinkDisabler from '../mixin/brokenLinkMixin';
import tooltipPositioner from '../mixin/dynamicTooltipMixin';
import cRamp from './c-ramp.vue';
import cStackedBarChart from './c-stacked-bar-chart.vue';
+import cMarkdownChunk from './c-markdown-chunk.vue';
import { Bar, Repo, User } from '../types/types';
import { FilterGroupSelection, FilterTimeFrame, SortGroupSelection } from '../types/summary';
import { StoreState, ZoomInfo } from '../types/vuex.d';
@@ -347,6 +355,7 @@ export default defineComponent({
components: {
cRamp,
cStackedBarChart,
+ cMarkdownChunk,
},
mixins: [brokenLinkDisabler, tooltipPositioner],
props: {
@@ -984,10 +993,41 @@ export default defineComponent({
return [...new Set(repo.flatMap((r) => r.commits).flatMap((c) => c.commitResults).flatMap((r) => r.tags))]
.filter(Boolean) as Array;
},
+
+ getBlurb(repo: User): string {
+ const link = this.getRepoLink(repo);
+ if (!link) {
+ return '';
+ }
+ const blurb: string | undefined = this.$store.state.blurbMap[link];
+ if (!blurb) {
+ return '';
+ }
+ return blurb;
+ },
},
});
diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts
index c69d2a4204..2781cf2e8c 100644
--- a/frontend/src/store/store.ts
+++ b/frontend/src/store/store.ts
@@ -18,6 +18,7 @@ export default createStore({
loadingOverlayCount: 0,
loadingOverlayMessage: '',
isTabActive: true,
+ blurbMap: {},
} as StoreState,
mutations: {
updateTabZoomInfo(state: StoreState, info: ZoomInfo) {
@@ -82,6 +83,9 @@ export default createStore({
file.wasCodeLoaded = file.wasCodeLoaded || file.active;
});
},
+ setBlurbMap(state: StoreState, blurbMap: { [key: string]: string }) {
+ state.blurbMap = blurbMap;
+ },
},
actions: {
// Actions are called with dispatch
diff --git a/frontend/src/types/vuex.d.ts b/frontend/src/types/vuex.d.ts
index cedee8c21d..abf06d15b8 100644
--- a/frontend/src/types/vuex.d.ts
+++ b/frontend/src/types/vuex.d.ts
@@ -47,6 +47,7 @@ interface StoreState {
loadingOverlayCount: number;
loadingOverlayMessage: string;
isTabActive: boolean;
+ blurbMap: { [key: string]: string };
}
declare module '@vue/runtime-core' {
diff --git a/frontend/src/types/window.ts b/frontend/src/types/window.ts
index b8a0a60524..dd2f1abd5e 100644
--- a/frontend/src/types/window.ts
+++ b/frontend/src/types/window.ts
@@ -24,6 +24,7 @@ interface Api {
reportGenerationTime: string;
errorMessages: { [key: string]: ErrorMessage };
names: string[];
+ blurbMap: { [key: string]: string };
} | null>;
loadCommits: (repoName: string) => Promise;
loadAuthorship: (repoName: string) => Promise;
diff --git a/frontend/src/types/zod/summary-type.ts b/frontend/src/types/zod/summary-type.ts
index 4ee1635406..316ce68f1b 100644
--- a/frontend/src/types/zod/summary-type.ts
+++ b/frontend/src/types/zod/summary-type.ts
@@ -45,6 +45,9 @@ export const summarySchema = z.object({
isUntilDateProvided: z.boolean(),
isAuthorshipAnalyzed: z.boolean().default(false), // for backwards compatability
supportedDomainUrlMap: supportedDomainUrlMapSchema,
+ blurbs: z.object({
+ urlBlurbMap: z.record(z.string(), z.string()),
+ }),
});
// Export typescript types
diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts
index 940dedffa7..573180fc89 100644
--- a/frontend/src/utils/api.ts
+++ b/frontend/src/utils/api.ts
@@ -233,11 +233,14 @@ window.api = {
window.REPOS[repoName] = repo;
names.push(repoName);
});
+
+ const blurbMap: { [key: string]: string } = data.blurbs.urlBlurbMap;
return {
creationDate: reportGeneratedTime,
reportGenerationTime,
errorMessages,
names,
+ blurbMap,
};
},
diff --git a/src/main/java/reposense/RepoSense.java b/src/main/java/reposense/RepoSense.java
index be54dd663b..36daa4d751 100644
--- a/src/main/java/reposense/RepoSense.java
+++ b/src/main/java/reposense/RepoSense.java
@@ -10,6 +10,7 @@
import net.sourceforge.argparse4j.helper.HelpScreenException;
import reposense.git.GitConfig;
+import reposense.model.BlurbMap;
import reposense.model.CliArguments;
import reposense.model.RepoConfiguration;
import reposense.model.ReportConfiguration;
@@ -17,6 +18,7 @@
import reposense.parser.ArgsParser;
import reposense.parser.exceptions.InvalidCsvException;
import reposense.parser.exceptions.InvalidHeaderException;
+import reposense.parser.exceptions.InvalidMarkdownException;
import reposense.parser.exceptions.ParseException;
import reposense.report.ReportGenerator;
import reposense.system.LogsManager;
@@ -43,6 +45,7 @@ public static void main(String[] args) {
CliArguments cliArguments = ArgsParser.parse(args);
List configs = null;
ReportConfiguration reportConfig = new ReportConfiguration();
+ BlurbMap blurbMap = new BlurbMap();
if (cliArguments.isViewModeOnly()) {
ReportServer.startServer(SERVER_PORT_NUMBER, cliArguments.getReportDirectoryPath().toAbsolutePath());
@@ -51,6 +54,7 @@ public static void main(String[] args) {
configs = RunConfigurationDecider.getRunConfiguration(cliArguments).getRepoConfigurations();
reportConfig = cliArguments.getReportConfiguration();
+ blurbMap = cliArguments.getBlurbMap();
RepoConfiguration.setFormatsToRepoConfigs(configs, cliArguments.getFormats());
RepoConfiguration.setDatesToRepoConfigs(configs, cliArguments.getSinceDate(), cliArguments.getUntilDate());
@@ -80,7 +84,9 @@ public static void main(String[] args) {
cliArguments.isSinceDateProvided(), cliArguments.isUntilDateProvided(),
cliArguments.getNumCloningThreads(), cliArguments.getNumAnalysisThreads(),
TimeUtil::getElapsedTime, cliArguments.getZoneId(), cliArguments.isFreshClonePerformed(),
- cliArguments.isAuthorshipAnalyzed(), cliArguments.getOriginalityThreshold());
+ cliArguments.isAuthorshipAnalyzed(), cliArguments.getOriginalityThreshold(),
+ blurbMap
+ );
FileUtil.zipFoldersAndFiles(reportFoldersAndFiles, cliArguments.getOutputFilePath().toAbsolutePath(),
".json");
@@ -97,6 +103,8 @@ public static void main(String[] args) {
logger.log(Level.WARNING, e.getMessage(), e);
} catch (HelpScreenException e) {
// help message was printed by the ArgumentParser; it is safe to exit.
+ } catch (InvalidMarkdownException ex) {
+ logger.log(Level.SEVERE, ex.getMessage(), ex);
}
LogsManager.moveLogFileToOutputFolder();
diff --git a/src/main/java/reposense/model/BlurbMap.java b/src/main/java/reposense/model/BlurbMap.java
new file mode 100644
index 0000000000..478a3d0c1f
--- /dev/null
+++ b/src/main/java/reposense/model/BlurbMap.java
@@ -0,0 +1,46 @@
+package reposense.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents the mapping between the repo URL to the associated blurb.
+ */
+public class BlurbMap {
+ @JsonProperty("urlBlurbMap")
+ private final Map urlBlurbMap;
+
+ public BlurbMap() {
+ this.urlBlurbMap = new HashMap<>();
+ }
+
+ public Map getAllMappings() {
+ return new HashMap<>(this.urlBlurbMap);
+ }
+
+ /**
+ * Adds a key-value record into the {@code BlurbMap}.
+ *
+ * @param key Key value.
+ * @param value Blurb value.
+ */
+ public void withRecord(String key, String value) {
+ this.urlBlurbMap.put(key, value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (obj instanceof BlurbMap) {
+ BlurbMap bm = (BlurbMap) obj;
+ return bm.urlBlurbMap.equals(this.urlBlurbMap);
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java
index d1b3564002..5f50e476de 100644
--- a/src/main/java/reposense/model/CliArguments.java
+++ b/src/main/java/reposense/model/CliArguments.java
@@ -51,6 +51,7 @@ public class CliArguments {
private Path groupConfigFilePath;
private Path reportConfigFilePath;
private ReportConfiguration reportConfiguration;
+ private BlurbMap blurbMap;
/**
* Constructs a {@code CliArguments} object without any parameters.
@@ -161,6 +162,10 @@ public ReportConfiguration getReportConfiguration() {
return reportConfiguration;
}
+ public BlurbMap getBlurbMap() {
+ return blurbMap;
+ }
+
public boolean isViewModeOnly() {
return isViewModeOnly;
}
@@ -211,6 +216,7 @@ public boolean equals(Object other) {
&& Objects.equals(this.authorConfigFilePath, otherCliArguments.authorConfigFilePath)
&& Objects.equals(this.groupConfigFilePath, otherCliArguments.groupConfigFilePath)
&& Objects.equals(this.reportConfigFilePath, otherCliArguments.reportConfigFilePath)
+ && Objects.equals(this.blurbMap, otherCliArguments.blurbMap)
&& this.isAuthorshipAnalyzed == otherCliArguments.isAuthorshipAnalyzed
&& Objects.equals(this.originalityThreshold, otherCliArguments.originalityThreshold);
}
@@ -487,6 +493,16 @@ public Builder originalityThreshold(double originalityThreshold) {
return this;
}
+ /**
+ * Adds the {@code blurbMap} to CliArguments.
+ *
+ * @param blurbMap The blurb map.
+ */
+ public Builder blurbMap(BlurbMap blurbMap) {
+ this.cliArguments.blurbMap = blurbMap;
+ return this;
+ }
+
/**
* Builds CliArguments.
*
diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java
index 35d6afc3a4..c6c3f34aa7 100644
--- a/src/main/java/reposense/parser/ArgsParser.java
+++ b/src/main/java/reposense/parser/ArgsParser.java
@@ -25,9 +25,11 @@
import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup;
import net.sourceforge.argparse4j.inf.Namespace;
import reposense.RepoSense;
+import reposense.model.BlurbMap;
import reposense.model.CliArguments;
import reposense.model.FileType;
import reposense.model.ReportConfiguration;
+import reposense.parser.exceptions.InvalidMarkdownException;
import reposense.parser.exceptions.ParseException;
import reposense.parser.types.AlphanumericArgumentType;
import reposense.parser.types.AnalysisThreadsArgumentType;
@@ -91,6 +93,7 @@ public class ArgsParser {
"Config path not provided, using the config folder as default.";
private static final String MESSAGE_INVALID_CONFIG_PATH = "%s is malformed.";
private static final String MESSAGE_INVALID_CONFIG_JSON = "%s Ignoring the report config provided.";
+ private static final String MESSAGE_INVALID_MARKDOWN_BLURBS = "%s Ignoring the blurb file provided.";
private static final String MESSAGE_SINCE_D1_WITH_PERIOD = "You may be using --since d1 with the --period flag. "
+ "This may result in an incorrect date range being analysed.";
private static final String MESSAGE_SINCE_DATE_LATER_THAN_UNTIL_DATE =
@@ -346,6 +349,7 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars
}
addReportConfigToBuilder(cliArgumentsBuilder, results);
+ addBlurbMapToBuilder(cliArgumentsBuilder, results);
addAnalysisDatesToBuilder(cliArgumentsBuilder, results);
boolean isViewModeOnly = reportFolderPath != null
@@ -397,6 +401,31 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names
builder.reportConfiguration(reportConfig);
}
+ /**
+ * Adds the blurbMap field to the given {@code builder}.
+ *
+ * @param builder Builder to be supplied with the reportConfig field.
+ * @param results Parsed results of the user-supplied CLI arguments.
+ */
+ private static void addBlurbMapToBuilder(CliArguments.Builder builder, Namespace results) {
+ BlurbMap blurbMap = new BlurbMap();
+ Path configFolderPath = results.get(CONFIG_FLAGS[0]);
+
+ // Blurbs are parsed regardless
+ Path blurbConfigPath = configFolderPath.resolve(BlurbMarkdownParser.DEFAULT_BLURB_FILENAME);
+
+ try {
+ blurbMap = new BlurbMarkdownParser(blurbConfigPath).parse();
+ } catch (InvalidMarkdownException ex) {
+ logger.warning(String.format(MESSAGE_INVALID_MARKDOWN_BLURBS, ex.getMessage()));
+ } catch (IOException ioe) {
+ // IOException thrown as blurbs.md is not found.
+ // Ignore exception as the file is optional.
+ }
+
+ builder.blurbMap(blurbMap);
+ }
+
/**
* Adds the sinceDate and untilDate fields for analysis to the given {@code builder}.
*
diff --git a/src/main/java/reposense/parser/BlurbMarkdownParser.java b/src/main/java/reposense/parser/BlurbMarkdownParser.java
new file mode 100644
index 0000000000..8c3e5a0f67
--- /dev/null
+++ b/src/main/java/reposense/parser/BlurbMarkdownParser.java
@@ -0,0 +1,151 @@
+package reposense.parser;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.regex.Pattern;
+
+import reposense.model.BlurbMap;
+import reposense.parser.exceptions.InvalidMarkdownException;
+
+/**
+ * Parses the Markdown file and retrieves the mappings from URLs to blurbs from the blurbs
+ * configuration file.
+ */
+public class BlurbMarkdownParser extends MarkdownParser {
+ public static final Pattern DELIMITER = Pattern.compile("(.*)");
+ public static final String DEFAULT_BLURB_FILENAME = "blurbs.md";
+
+ private static final class UrlRecord {
+ private final String url;
+ private final int nextPosition;
+
+ public UrlRecord(String url, int nextPosition) {
+ this.url = url;
+ this.nextPosition = nextPosition;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public int getNextPosition() {
+ return nextPosition;
+ }
+ }
+
+ private static final class BlurbRecord {
+ private final List blurb;
+ private final int nextPosition;
+
+ public BlurbRecord(List blurb, int nextPosition) {
+ this.blurb = blurb;
+ this.nextPosition = nextPosition;
+ }
+
+ public List getBlurb() {
+ return blurb;
+ }
+
+ public int getNextPosition() {
+ return nextPosition;
+ }
+ }
+
+ public BlurbMarkdownParser(Path markdownPath) throws FileNotFoundException {
+ super(markdownPath);
+ }
+
+ /**
+ * Parses the markdown file containing the url to blurb mapping and returns a
+ * {@code BlurbMap} containing the mappings between the url and blurbs.
+ *
+ * @return {@code BlurbMap} object.
+ * @throws IOException if there are any issues opening or parsing the {@code blurbs.md} file.
+ */
+ @Override
+ public BlurbMap parse() throws IOException, InvalidMarkdownException {
+ logger.log(Level.INFO, "Parsing Blurbs...");
+ // read all the lines first
+ List mdLines = Files.readAllLines(this.markdownPath);
+
+ // if the file is empty, then we throw the exception and let the adder handle
+ if (mdLines.isEmpty()) {
+ throw new InvalidMarkdownException("Empty blurbs.md file");
+ }
+
+ // prepare the blurb map
+ BlurbMap blurbMap = new BlurbMap();
+
+ // define temporary local variables to track blurbs
+ String url = "";
+ StringBuilder blurb = new StringBuilder();
+ int counter = 0;
+
+ while (counter < mdLines.size()) {
+ // extract the url record first
+ // this is guaranteed to be in the first line or else we fail
+ UrlRecord urlRecord = this.getUrlRecord(mdLines, counter);
+ url = urlRecord.getUrl();
+ counter = urlRecord.getNextPosition();
+
+ // then extract the blurb record next
+ // we extract until the delimiter is found and then we will stop
+ BlurbRecord blurbRecord = this.getBlurbRecord(mdLines, counter);
+ List blurbExtracted = blurbRecord.getBlurb();
+ for (String string : blurbExtracted) {
+ blurb.append(string);
+ }
+ counter = blurbRecord.getNextPosition();
+
+ // add the recorded entry into the BlurbMap
+ // strip the trailing /n
+ blurbMap.withRecord(url, blurb.toString().stripTrailing());
+ blurb.setLength(0);
+ }
+
+ // return the built BlurbMap instance
+ logger.log(Level.INFO, "Blurbs parsed successfully!");
+ return blurbMap;
+ }
+
+ private UrlRecord getUrlRecord(List lines, int position) throws InvalidMarkdownException {
+ // checks if url is valid
+ // adapted from https://www.baeldung.com/java-validate-url
+ try {
+ String url = lines.get(position).strip();
+ new URL(url).toURI();
+ return new UrlRecord(lines.get(position), position + 1);
+ } catch (MalformedURLException | URISyntaxException ex) {
+ throw new InvalidMarkdownException("URL provided is not valid!");
+ }
+ }
+
+ private BlurbRecord getBlurbRecord(List lines, int position) {
+ int lineSize = lines.size();
+ int posCounter = position;
+ List blurbs = new ArrayList<>();
+
+ while (posCounter < lineSize) {
+ String currLine = lines.get(posCounter);
+
+ if (BlurbMarkdownParser.DELIMITER.matcher(currLine).matches()) {
+ break;
+ } else {
+ currLine += "\n";
+ blurbs.add(currLine);
+ }
+
+ posCounter++;
+ }
+
+ return new BlurbRecord(blurbs, posCounter + 1);
+ }
+}
diff --git a/src/main/java/reposense/parser/MarkdownParser.java b/src/main/java/reposense/parser/MarkdownParser.java
new file mode 100644
index 0000000000..93ca3cb95d
--- /dev/null
+++ b/src/main/java/reposense/parser/MarkdownParser.java
@@ -0,0 +1,32 @@
+package reposense.parser;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Logger;
+
+import reposense.parser.exceptions.InvalidMarkdownException;
+import reposense.system.LogsManager;
+
+/**
+ * Parses Markdown file according to the "" tag.
+ *
+ * @param Generic Type {@code T}.
+ */
+public abstract class MarkdownParser {
+ protected static final Logger logger = LogsManager.getLogger(MarkdownParser.class);
+
+ protected Path markdownPath;
+
+ public MarkdownParser(Path markdownPath) throws FileNotFoundException {
+ if (markdownPath == null || !Files.exists(markdownPath)) {
+ throw new FileNotFoundException("Markdown file does not exist at the given path.\n"
+ + "Use '-help' to list all the available subcommands and some concept guides.");
+ }
+
+ this.markdownPath = markdownPath;
+ }
+
+ public abstract T parse() throws IOException, InvalidMarkdownException;
+}
diff --git a/src/main/java/reposense/parser/exceptions/InvalidMarkdownException.java b/src/main/java/reposense/parser/exceptions/InvalidMarkdownException.java
new file mode 100644
index 0000000000..fc6b2fd71a
--- /dev/null
+++ b/src/main/java/reposense/parser/exceptions/InvalidMarkdownException.java
@@ -0,0 +1,10 @@
+package reposense.parser.exceptions;
+
+/**
+ * Represents the error thrown when Markdown files cannot be parsed.
+ */
+public class InvalidMarkdownException extends Exception {
+ public InvalidMarkdownException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/reposense/report/ReportGenerator.java b/src/main/java/reposense/report/ReportGenerator.java
index fd1f9744bd..ab0b19ec6e 100644
--- a/src/main/java/reposense/report/ReportGenerator.java
+++ b/src/main/java/reposense/report/ReportGenerator.java
@@ -41,12 +41,14 @@
import reposense.git.exception.GitBranchException;
import reposense.git.exception.GitCloneException;
import reposense.model.Author;
+import reposense.model.BlurbMap;
import reposense.model.CommitHash;
import reposense.model.RepoConfiguration;
import reposense.model.RepoLocation;
import reposense.model.ReportConfiguration;
import reposense.model.StandaloneConfig;
import reposense.parser.StandaloneConfigJsonParser;
+import reposense.parser.exceptions.InvalidMarkdownException;
import reposense.report.exception.NoAuthorsWithCommitsFoundException;
import reposense.system.LogsManager;
import reposense.util.FileUtil;
@@ -112,14 +114,17 @@ public class ReportGenerator {
* @param shouldFreshClone The boolean variable for whether to clone a repo again during tests.
* @param shouldAnalyzeAuthorship The boolean variable for whether to further analyze authorship.
* @param originalityThreshold The double variable for originality threshold in analyze authorship.
+ * @param blurbMap The {@code BlurbMap}.
* @return the list of file paths that were generated.
* @throws IOException if templateZip.zip does not exist in jar file.
+ * @throws InvalidMarkdownException if the blurb markdown file cannot be parsed properly.
*/
public List generateReposReport(List configs, String outputPath, String assetsPath,
ReportConfiguration reportConfig, String generationDate, LocalDateTime cliSinceDate,
LocalDateTime untilDate, boolean isSinceDateProvided, boolean isUntilDateProvided, int numCloningThreads,
int numAnalysisThreads, Supplier reportGenerationTimeProvider, ZoneId zoneId,
- boolean shouldFreshClone, boolean shouldAnalyzeAuthorship, double originalityThreshold) throws IOException {
+ boolean shouldFreshClone, boolean shouldAnalyzeAuthorship, double originalityThreshold, BlurbMap blurbMap)
+ throws IOException, InvalidMarkdownException {
prepareTemplateFile(outputPath);
if (Files.exists(Paths.get(assetsPath))) {
FileUtil.copyDirectoryContents(assetsPath, outputPath, assetsFilesWhiteList);
@@ -138,7 +143,7 @@ public List generateReposReport(List configs, String ou
new SummaryJson(configs, reportConfig, generationDate,
reportSinceDate, untilDate, isSinceDateProvided,
isUntilDateProvided, RepoSense.getVersion(), ErrorSummary.getInstance().getErrorSet(),
- reportGenerationTimeProvider.get(), zoneId, shouldAnalyzeAuthorship),
+ reportGenerationTimeProvider.get(), zoneId, shouldAnalyzeAuthorship, blurbMap),
getSummaryResultPath(outputPath));
summaryPath.ifPresent(reportFoldersAndFiles::add);
diff --git a/src/main/java/reposense/report/SummaryJson.java b/src/main/java/reposense/report/SummaryJson.java
index f709fb0998..1e65acad2b 100644
--- a/src/main/java/reposense/report/SummaryJson.java
+++ b/src/main/java/reposense/report/SummaryJson.java
@@ -6,6 +6,7 @@
import java.util.Map;
import java.util.Set;
+import reposense.model.BlurbMap;
import reposense.model.RepoConfiguration;
import reposense.model.ReportConfiguration;
import reposense.model.SupportedDomainUrlMap;
@@ -29,11 +30,13 @@ public class SummaryJson {
private final boolean isUntilDateProvided;
private final Map> supportedDomainUrlMap;
private final boolean isAuthorshipAnalyzed;
+ private final BlurbMap blurbs;
public SummaryJson(List repos, ReportConfiguration reportConfig, String reportGeneratedTime,
- LocalDateTime sinceDate, LocalDateTime untilDate, boolean isSinceDateProvided, boolean isUntilDateProvided,
- String repoSenseVersion, Set