Skip to content

Commit d31629e

Browse files
authored
Merge pull request #55 from InnerSourceCommons/add-gqm-gen
feat: add gqm_gen script
2 parents e441b35 + f9a1923 commit d31629e

File tree

12 files changed

+1032
-1
lines changed

12 files changed

+1032
-1
lines changed

measuring/questions/who-uses.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[⬑ back to the overall graph](../use_gqm.md)
22

3-
# Question: Who uses the InnerSource project?
3+
# **Question:** Who uses the InnerSource project?
44

55
Depending on the InnerSource project, usage of the project could look something like:
66

package-lock.json

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

scripts/gqm_gen/.gitignore

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
6+
# Runtime data
7+
pids
8+
*.pid
9+
*.seed
10+
*.pid.lock
11+
12+
# Directory for instrumented libs generated by jscoverage/JSCover
13+
lib-cov
14+
15+
# Coverage directory used by tools like istanbul
16+
coverage
17+
18+
# nyc test coverage
19+
.nyc_output
20+
21+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
22+
.grunt
23+
24+
# node-waf configuration
25+
.lock-wscript
26+
27+
# Compiled binary addons (http://nodejs.org/api/addons.html)
28+
build/Release
29+
30+
# Dependency directories
31+
node_modules
32+
jspm_packages
33+
34+
# Optional npm cache directory
35+
.npm
36+
37+
# Optional REPL history
38+
.node_repl_history
39+
40+
# TypeScript
41+
*.map
42+
*.js
43+
44+
# Project generated files
45+
*.pdf
46+
*.xlsx
47+
slides/*.md

scripts/gqm_gen/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Goal, Question, Metrics Generator
2+
3+
Creates a Mermaid diagram from Goal, Question, Metrics (GQM) Markdown files.
4+
5+
## Usage
6+
7+
Builds the mermaid diagram and outputs it to stdout.
8+
9+
```bash
10+
npm install
11+
npm run --silent start
12+
```

scripts/gqm_gen/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Commonmark from 'commonmark';
2+
export declare function getLinks(parsed: Commonmark.Node): {
3+
url: string | null;
4+
text: string | null | undefined;
5+
}[];

scripts/gqm_gen/index.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as Commonmark from "commonmark";
2+
import test from "node:test";
3+
import assert from "node:assert";
4+
import { FileLink, Graph, LinkType } from "./types";
5+
import {
6+
getLinks,
7+
getFileLinks,
8+
generateMermaidDiagram,
9+
getGQMFileLinks,
10+
} from "./index";
11+
12+
test("can get links", () => {
13+
const node = new Commonmark.Node("paragraph");
14+
const links = getLinks(node);
15+
assert(links.length === 0);
16+
});
17+
18+
test("can get links from file", () => {
19+
const fileLinks: FileLink[] = getFileLinks(
20+
LinkType.GOAL,
21+
"../../measuring/goals/"
22+
);
23+
assert(fileLinks.length > 0);
24+
});
25+
26+
test("can generate mermaid diagram", () => {
27+
const graph: Graph = {
28+
nodes: [],
29+
edges: [],
30+
};
31+
const diagram = generateMermaidDiagram(graph);
32+
assert(diagram.indexOf("graph TB") > 1);
33+
});
34+
35+
test("can generate mermaid diagram from file", { only: true }, () => {
36+
const graph = getGQMFileLinks();
37+
38+
const diagram = generateMermaidDiagram(graph);
39+
console.log(diagram);
40+
assert(diagram.indexOf("graph LR;") > 1);
41+
});

scripts/gqm_gen/index.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import * as Commonmark from "commonmark";
2+
import * as fs from "fs";
3+
import * as path from "path";
4+
import { Node, Edge, Graph, Link, LinkType, NodeShape, ArrowType } from "./types";
5+
import { FileLink } from "./types";
6+
7+
const graph = getGQMFileLinks();
8+
9+
export function getLinkUrl(linkType: LinkType, file: string) {
10+
const measuringUrl = "https://github.com/InnerSourceCommons/managing-inner-source-projects/blob/main/measuring/";
11+
const url = `${measuringUrl}/${linkType.toLowerCase()}s/${file}`
12+
return url;
13+
}
14+
15+
export function getGQMFileLinks() {
16+
const graph: Graph = {
17+
nodes: [],
18+
edges: [],
19+
};
20+
21+
const goalsPath = "../../measuring/goals/";
22+
const questionsPath = "../../measuring/questions/";
23+
const metricsPath = "../../measuring/metrics/";
24+
25+
const goalFileLinks: FileLink[] = getFileLinks(LinkType.GOAL, goalsPath);
26+
appendToGraph(graph, goalFileLinks);
27+
28+
const questionFileLinks: FileLink[] = getFileLinks(
29+
LinkType.QUESTION,
30+
questionsPath
31+
);
32+
appendToGraph(graph, questionFileLinks);
33+
34+
const metricFileLinks: FileLink[] = getFileLinks(LinkType.METRIC, metricsPath);
35+
appendToGraph(graph, metricFileLinks);
36+
37+
return graph;
38+
}
39+
40+
export function appendToGraph(graph: Graph, fileLinks: FileLink[]) {
41+
fileLinks.forEach((fileLink) => {
42+
const node: Node = {
43+
id: fileLink.file,
44+
type: fileLink.linkType,
45+
shape: NodeShape.RECT,
46+
label: fileLink.label,
47+
};
48+
if (node.label) {
49+
graph.nodes.push(node);
50+
}
51+
52+
fileLink.links.forEach((link) => {
53+
const edge: Edge = {
54+
from: fileLink.file,
55+
to: link.name,
56+
arrowType: "arrow",
57+
};
58+
if(edge.from && edge.to){
59+
graph.edges.push(edge);
60+
}
61+
});
62+
});
63+
return graph
64+
}
65+
66+
export function getFileLinks(linkType: LinkType, filePath: string) {
67+
const parser = new Commonmark.Parser();
68+
69+
const goalFiles = fs.readdirSync(filePath);
70+
let fileLinks: FileLink[] = [];
71+
goalFiles.forEach((fileName) => {
72+
if (fileName.endsWith("template.md")) return;
73+
const data = fs.readFileSync(`${filePath}/${fileName}`, "utf-8");
74+
const parsed = parser.parse(data);
75+
const label = getHeading(parsed);
76+
const links = getLinks(parsed);
77+
78+
const fileLink: FileLink = {
79+
linkType: linkType,
80+
file: fileName,
81+
label: label,
82+
links,
83+
};
84+
fileLinks.push(fileLink);
85+
});
86+
return fileLinks;
87+
}
88+
89+
export function getHeading(parsed: Commonmark.Node) {
90+
const walker = parsed.walker();
91+
let event, node;
92+
let heading: string = "No Heading";
93+
while ((event = walker.next())) {
94+
node = event.node;
95+
if (event.entering && node.type === "heading") {
96+
heading = node.lastChild?.literal as string;
97+
break;
98+
}
99+
}
100+
return heading.trim();
101+
}
102+
103+
export function getLinks(parsed: Commonmark.Node) {
104+
const walker = parsed.walker();
105+
let event, node;
106+
const links: Link[] = [];
107+
while ((event = walker.next())) {
108+
node = event.node;
109+
if (event.entering && node.type === "link") {
110+
const destination = node.destination as string;
111+
const text = node.firstChild?.literal as string;
112+
const link: Link = {
113+
url: destination,
114+
text: text,
115+
name: path.parse(destination).base,
116+
};
117+
118+
if (link.url.indexOf('.md') > -1 && link.url.indexOf('use_gqm') === -1) {
119+
links.push(link);
120+
}
121+
}
122+
}
123+
return links;
124+
}
125+
126+
export function getNodeShapeSyntax(node: Node) {
127+
const nodeUrl = getLinkUrl(node.type, node.id)
128+
const nodeLabel = `<a href='${nodeUrl}'>${node.label}</a>`;
129+
switch (node.shape) {
130+
case 'rect':
131+
return `[${nodeLabel}]`;
132+
case 'circ':
133+
return `((${nodeLabel}))`;
134+
case 'roundrect':
135+
return `((${nodeLabel}))`;
136+
case 'diamond':
137+
return `{${nodeLabel}}`;
138+
default:
139+
return `[${nodeLabel}]`;
140+
}
141+
}
142+
143+
export function generateMermaidDiagram(graph: Graph) {
144+
const nodes = graph.nodes;
145+
const edges = graph.edges;
146+
147+
let mermaidSyntax = `\`\`\`mermaid\n
148+
graph LR;\n
149+
subgraph GQM[Goals, Questions, Metrics]\n
150+
`;
151+
152+
nodes.forEach((node) => {
153+
const nodeSyntax = getNodeShapeSyntax(node)
154+
mermaidSyntax += ` ${node.id}${nodeSyntax}\n`
155+
});
156+
157+
edges.forEach((edge) => {
158+
const arrowSyntax: string = ArrowType.ARROW;
159+
mermaidSyntax += `${edge.from}${arrowSyntax}${edge.to}\n`;
160+
});
161+
162+
const goalsList = nodes.filter(n => n.type == LinkType.GOAL).map(n => `${n.id}`).join(',');
163+
const questionsList = nodes.filter(n => n.type == LinkType.QUESTION).map(n => `${n.id}`).join(',');
164+
const metricsList = nodes.filter(n => n.type == LinkType.METRIC).map(n => `${n.id}`).join(',');
165+
166+
mermaidSyntax += " end";
167+
mermaidSyntax += `
168+
subgraph Legend
169+
direction TB
170+
171+
goal[Goal]
172+
question[Question]
173+
metric[Metric]
174+
175+
classDef goals stroke:green,stroke-width:2px;
176+
class goal,${goalsList} goals
177+
178+
classDef questions stroke:orange,stroke-width:2px;
179+
class question,${questionsList} questions
180+
181+
classDef metrics stroke:purple,stroke-width:2px;
182+
class metric,${metricsList} metrics
183+
end
184+
`;
185+
mermaidSyntax += "\n```";
186+
return mermaidSyntax;
187+
}
188+
189+
console.log(generateMermaidDiagram(graph))

0 commit comments

Comments
 (0)